From af3498c3089100d54a3c7417c7929b7f9687542b Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 4 Oct 2024 10:07:11 -0700 Subject: [PATCH 01/65] working Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 86 +++ .../radius/radapp.io_deploymenttemplates.yaml | 100 ++++ deploy/Chart/templates/controller/rbac.yaml | 4 + example/deploymenttemplate.yaml | 65 +++ example/env.bicep | 24 + example/env.bicepparam | 3 + example/env.json | 52 ++ example/env.parameters.json | 9 + pkg/cli/deployment/deploy.go | 1 + .../v1alpha3/deploymentresource_types.go | 87 +++ .../v1alpha3/deploymenttemplate_types.go | 99 ++++ .../v1alpha3/zz_generated.deepcopy.go | 193 +++++++ pkg/controller/reconciler/const.go | 6 + .../deploymentresource_reconciler.go | 247 +++++++++ .../deploymenttemplate_reconciler.go | 522 ++++++++++++++++++ pkg/controller/reconciler/util.go | 16 + pkg/controller/service.go | 18 + 17 files changed, 1532 insertions(+) create mode 100644 deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml create mode 100644 deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml create mode 100644 example/deploymenttemplate.yaml create mode 100644 example/env.bicep create mode 100644 example/env.bicepparam create mode 100644 example/env.json create mode 100644 example/env.parameters.json create mode 100644 pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go create mode 100644 pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go create mode 100644 pkg/controller/reconciler/deploymentresource_reconciler.go create mode 100644 pkg/controller/reconciler/deploymenttemplate_reconciler.go diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml new file mode 100644 index 0000000000..f38cbecc31 --- /dev/null +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: deploymentresources.radapp.io +spec: + group: radapp.io + names: + kind: DeploymentResource + listKind: DeploymentResourceList + plural: deploymentresources + singular: deploymentresource + scope: Namespaced + versions: + - name: v1alpha3 + schema: + openAPIV3Schema: + description: DeploymentResource is the Schema for the DeploymentResources + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DeploymentResourceSpec defines the desired state of DeploymentResource + properties: + resourceId: + description: ResourceId is the Radius resource Id. + type: string + required: + - resourceId + type: object + status: + description: DeploymentResourceStatus defines the observed state of DeploymentResource + properties: + message: + description: Message is a human-readable description of the status + of the Deployment Resource. + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this DeploymentResource. + format: int64 + type: integer + operation: + description: Operation tracks the status of an in-progress provisioning + operation. + properties: + operationKind: + description: OperationKind describes the type of operation being + performed. + type: string + resumeToken: + description: ResumeToken is a token that can be used to resume + an in-progress provisioning operation. + type: string + type: object + phrase: + description: Phrase indicates the current status of the Deployment + Resource. + type: string + scope: + description: Scope is the resource ID of the scope. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml new file mode 100644 index 0000000000..9d3c1fe987 --- /dev/null +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -0,0 +1,100 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: deploymenttemplates.radapp.io +spec: + group: radapp.io + names: + kind: DeploymentTemplate + listKind: DeploymentTemplateList + plural: deploymenttemplates + singular: deploymenttemplate + scope: Namespaced + versions: + - name: v1alpha3 + schema: + openAPIV3Schema: + description: DeploymentTemplate is the Schema for the deploymenttemplates + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: DeploymentTemplateSpec defines the desired state of DeploymentTemplate + properties: + parameters: + description: Parameters is the ARM JSON parameters for the template. + type: string + template: + description: Template is the ARM JSON manifest that defines the resources + to deploy. + type: string + required: + - parameters + - template + type: object + status: + description: DeploymentTemplateStatus defines the observed state of DeploymentTemplate + properties: + message: + description: Message is a human-readable description of the status + of the Deployment Template. + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this DeploymentTemplate. + format: int64 + type: integer + operation: + description: Operation tracks the status of an in-progress provisioning + operation. + properties: + operationKind: + description: OperationKind describes the type of operation being + performed. + type: string + resumeToken: + description: ResumeToken is a token that can be used to resume + an in-progress provisioning operation. + type: string + type: object + outputResources: + description: OutputResources is a list of the resourceIds that were + created by the template. + items: + type: string + type: array + phrase: + description: Phrase indicates the current status of the Deployment + Template. + type: string + resource: + description: Resource is the resource ID of the deployment. + type: string + scope: + description: Scope is the resource ID of the scope. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/Chart/templates/controller/rbac.yaml b/deploy/Chart/templates/controller/rbac.yaml index b060d31744..db554aa8d5 100644 --- a/deploy/Chart/templates/controller/rbac.yaml +++ b/deploy/Chart/templates/controller/rbac.yaml @@ -38,6 +38,10 @@ rules: resources: - recipes - recipes/status + - deploymenttemplates + - deploymenttemplates/status + - deploymentresources + - deploymentresources/status verbs: - create - delete diff --git a/example/deploymenttemplate.yaml b/example/deploymenttemplate.yaml new file mode 100644 index 0000000000..b95209df45 --- /dev/null +++ b/example/deploymenttemplate.yaml @@ -0,0 +1,65 @@ +kind: DeploymentTemplate +apiVersion: radapp.io/v1alpha3 +metadata: + name: env.bicep + namespace: radius-system +spec: + template: | + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "1.9-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "9193032425594528343" + } + }, + "parameters": { + "kubernetesNamespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "env", + "location": "global", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('kubernetesNamespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "testrecipe": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36" + } + } + } + } + } + } + } + } + parameters: | + { + "kubernetesNamespace": { + "value": "default" + } + } diff --git a/example/env.bicep b/example/env.bicep new file mode 100644 index 0000000000..a67fdb93b8 --- /dev/null +++ b/example/env.bicep @@ -0,0 +1,24 @@ +// Import the set of Radius resources (Applications.*) into Bicep +extension radius + +param kubernetesNamespace string + +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: 'env' + location: 'global' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: kubernetesNamespace + } + recipes: { + 'Applications.Datastores/redisCaches': { + testrecipe: { + templateKind: 'bicep' + templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36' + } + } + } + } +} diff --git a/example/env.bicepparam b/example/env.bicepparam new file mode 100644 index 0000000000..999bbc731b --- /dev/null +++ b/example/env.bicepparam @@ -0,0 +1,3 @@ +using 'env.bicep' + +param kubernetesNamespace = 'default' diff --git a/example/env.json b/example/env.json new file mode 100644 index 0000000000..77462fe66a --- /dev/null +++ b/example/env.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "1.9-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "9193032425594528343" + } + }, + "parameters": { + "kubernetesNamespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "env", + "location": "global", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('kubernetesNamespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "testrecipe": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/example/env.parameters.json b/example/env.parameters.json new file mode 100644 index 0000000000..61e194d37e --- /dev/null +++ b/example/env.parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "kubernetesNamespace": { + "value": "default" + } + } +} \ No newline at end of file diff --git a/pkg/cli/deployment/deploy.go b/pkg/cli/deployment/deploy.go index b00bd0557d..1c53e08d03 100644 --- a/pkg/cli/deployment/deploy.go +++ b/pkg/cli/deployment/deploy.go @@ -116,6 +116,7 @@ func (dc *ResourceDeploymentClient) startDeployment(ctx context.Context, name st resourceId = ucpresources.MakeUCPID(scopes, types, nil) providerConfig := dc.GetProviderConfigs(options) + //TODOWILLSMITH: reference poller, err := dc.Client.CreateOrUpdate(ctx, sdkclients.Deployment{ Properties: &sdkclients.DeploymentProperties{ diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go new file mode 100644 index 0000000000..6666a26143 --- /dev/null +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -0,0 +1,87 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeploymentResourceSpec defines the desired state of DeploymentResource +type DeploymentResourceSpec struct { + // ResourceId is the Radius resource Id. + ResourceId string `json:"resourceId"` +} + +// DeploymentResourceStatus defines the observed state of DeploymentResource +type DeploymentResourceStatus struct { + // Scope is the resource ID of the scope. + Scope string `json:"scope,omitempty"` + + // ObservedGeneration is the most recent generation observed for this DeploymentResource. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Operation tracks the status of an in-progress provisioning operation. + Operation *ResourceOperation `json:"operation,omitempty"` + + // Phrase indicates the current status of the Deployment Resource. + Phrase DeploymentResourcePhrase `json:"phrase,omitempty"` + + // Message is a human-readable description of the status of the Deployment Resource. + Message string `json:"message,omitempty"` +} + +// DeploymentResourcePhrase is a string representation of the current status of a Deployment Resource. +type DeploymentResourcePhrase string + +const ( + // DeploymentResourcePhraseReady indicates that the Deployment Resource is ready. + DeploymentResourcePhraseReady DeploymentResourcePhrase = "Ready" + + // DeploymentResourcePhraseFailed indicates that the Deployment Resource has failed. + DeploymentResourcePhraseFailed DeploymentResourcePhrase = "Failed" + + // DeploymentResourcePhraseDeleting indicates that the Deployment Resource is being deleted. + DeploymentResourcePhraseDeleting DeploymentResourcePhrase = "Deleting" + + // DeploymentResourcePhraseDeleted indicates that the Deployment Resource has been deleted. + DeploymentResourcePhraseDeleted DeploymentResourcePhrase = "Deleted" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// DeploymentResource is the Schema for the DeploymentResources API +type DeploymentResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeploymentResourceSpec `json:"spec,omitempty"` + Status DeploymentResourceStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DeploymentResourceList contains a list of DeploymentResource +type DeploymentResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeploymentResource `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeploymentResource{}, &DeploymentResourceList{}) +} diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go new file mode 100644 index 0000000000..546fa7f247 --- /dev/null +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -0,0 +1,99 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeploymentTemplateSpec defines the desired state of DeploymentTemplate +type DeploymentTemplateSpec struct { + // Template is the ARM JSON manifest that defines the resources to deploy. + Template string `json:"template"` + + // Parameters is the ARM JSON parameters for the template. + Parameters string `json:"parameters"` +} + +// DeploymentTemplateStatus defines the observed state of DeploymentTemplate +type DeploymentTemplateStatus struct { + // ObservedGeneration is the most recent generation observed for this DeploymentTemplate. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Scope is the resource ID of the scope. + Scope string `json:"scope,omitempty"` + + // Resource is the resource ID of the deployment. + Resource string `json:"resource,omitempty"` + + // OutputResources is a list of the resourceIds that were created by the template. + OutputResources []string `json:"outputResources,omitempty"` + + // Operation tracks the status of an in-progress provisioning operation. + Operation *ResourceOperation `json:"operation,omitempty"` + + // Phrase indicates the current status of the Deployment Template. + Phrase DeploymentTemplatePhrase `json:"phrase,omitempty"` + + // Message is a human-readable description of the status of the Deployment Template. + Message string `json:"message,omitempty"` +} + +// DeploymentTemplatePhrase is a string representation of the current status of a Deployment Template. +type DeploymentTemplatePhrase string + +const ( + // DeploymentTemplatePhraseUpdating indicates that the Deployment Template is being updated. + DeploymentTemplatePhraseUpdating DeploymentTemplatePhrase = "Updating" + + // DeploymentTemplatePhraseReady indicates that the Deployment Template is ready. + DeploymentTemplatePhraseReady DeploymentTemplatePhrase = "Ready" + + // DeploymentTemplatePhraseFailed indicates that the Deployment Template has failed. + DeploymentTemplatePhraseFailed DeploymentTemplatePhrase = "Failed" + + // DeploymentTemplatePhraseDeleting indicates that the Deployment Template is being deleted. + DeploymentTemplatePhraseDeleting DeploymentTemplatePhrase = "Deleting" + + // DeploymentTemplatePhraseDeleted indicates that the Deployment Template has been deleted. + DeploymentTemplatePhraseDeleted DeploymentTemplatePhrase = "Deleted" +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// DeploymentTemplate is the Schema for the deploymenttemplates API +type DeploymentTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DeploymentTemplateSpec `json:"spec,omitempty"` + Status DeploymentTemplateStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DeploymentTemplateList contains a list of DeploymentTemplate +type DeploymentTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []DeploymentTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&DeploymentTemplate{}, &DeploymentTemplateList{}) +} diff --git a/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go b/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go index 90116af1dc..aa25b10470 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go +++ b/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go @@ -24,6 +24,199 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentResource) DeepCopyInto(out *DeploymentResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentResource. +func (in *DeploymentResource) DeepCopy() *DeploymentResource { + if in == nil { + return nil + } + out := new(DeploymentResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentResourceList) DeepCopyInto(out *DeploymentResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeploymentResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentResourceList. +func (in *DeploymentResourceList) DeepCopy() *DeploymentResourceList { + if in == nil { + return nil + } + out := new(DeploymentResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentResourceSpec) DeepCopyInto(out *DeploymentResourceSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentResourceSpec. +func (in *DeploymentResourceSpec) DeepCopy() *DeploymentResourceSpec { + if in == nil { + return nil + } + out := new(DeploymentResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentResourceStatus) DeepCopyInto(out *DeploymentResourceStatus) { + *out = *in + if in.Operation != nil { + in, out := &in.Operation, &out.Operation + *out = new(ResourceOperation) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentResourceStatus. +func (in *DeploymentResourceStatus) DeepCopy() *DeploymentResourceStatus { + if in == nil { + return nil + } + out := new(DeploymentResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentTemplate) DeepCopyInto(out *DeploymentTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplate. +func (in *DeploymentTemplate) DeepCopy() *DeploymentTemplate { + if in == nil { + return nil + } + out := new(DeploymentTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentTemplateList) DeepCopyInto(out *DeploymentTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DeploymentTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplateList. +func (in *DeploymentTemplateList) DeepCopy() *DeploymentTemplateList { + if in == nil { + return nil + } + out := new(DeploymentTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DeploymentTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentTemplateSpec) DeepCopyInto(out *DeploymentTemplateSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplateSpec. +func (in *DeploymentTemplateSpec) DeepCopy() *DeploymentTemplateSpec { + if in == nil { + return nil + } + out := new(DeploymentTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentTemplateStatus) DeepCopyInto(out *DeploymentTemplateStatus) { + *out = *in + if in.OutputResources != nil { + in, out := &in.OutputResources, &out.OutputResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Operation != nil { + in, out := &in.Operation, &out.Operation + *out = new(ResourceOperation) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplateStatus. +func (in *DeploymentTemplateStatus) DeepCopy() *DeploymentTemplateStatus { + if in == nil { + return nil + } + out := new(DeploymentTemplateStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Recipe) DeepCopyInto(out *Recipe) { *out = *in diff --git a/pkg/controller/reconciler/const.go b/pkg/controller/reconciler/const.go index b2f3739f13..77a39df103 100644 --- a/pkg/controller/reconciler/const.go +++ b/pkg/controller/reconciler/const.go @@ -47,4 +47,10 @@ const ( // RecipeFinalizer is the name of the finalizer added to Recipes. RecipeFinalizer = "radapp.io/recipe-finalizer" + + // DeploymentTemplateFinalizer is the name of the finalizer added to DeploymentTemplates. + DeploymentTemplateFinalizer = "radapp.io/deployment-template-finalizer" + + // DeploymentResourceFinalizer is the name of the finalizer added to DeploymentResources. + DeploymentResourceFinalizer = "radapp.io/deployment-resource-finalizer" ) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go new file mode 100644 index 0000000000..e21f507dcd --- /dev/null +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -0,0 +1,247 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/go-logr/logr" + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/pkg/ucp/ucplog" + corev1 "k8s.io/api/core/v1" +) + +// DeploymentResourceReconciler reconciles a DeploymentResource object. +type DeploymentResourceReconciler struct { + // Client is the Kubernetes client. + Client client.Client + + // Scheme is the Kubernetes scheme. + Scheme *runtime.Scheme + + // EventRecorder is the Kubernetes event recorder. + EventRecorder record.EventRecorder + + // Radius is the Radius client. + Radius RadiusClient + + // DelayInterval is the amount of time to wait between operations. + DelayInterval time.Duration +} + +// Reconcile is the main reconciliation loop for the DeploymentResource resource. +func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "DeploymentResource", "name", req.Name, "namespace", req.Namespace) + ctx = logr.NewContext(ctx, logger) + + DeploymentResource := radappiov1alpha3.DeploymentResource{} + err := r.Client.Get(ctx, req.NamespacedName, &DeploymentResource) + if apierrors.IsNotFound(err) { + // This can happen due to a data-race if the Deployment Resource is created and then deleted before we can + // reconcile it. There's nothing to do here. + logger.Info("DeploymentResource is being deleted.") + return ctrl.Result{}, nil + } else if err != nil { + logger.Error(err, "Unable to fetch resource.") + return ctrl.Result{}, err + } + + // Our algorithm is as follows: + // + // TODOWILLSMITH: put algorithm here + // + // We do it this way because it guarantees that we only have one operation going at a time. + + if DeploymentResource.Status.Operation != nil { + result, err := r.reconcileOperation(ctx, &DeploymentResource) + if err != nil { + logger.Error(err, "Unable to reconcile in-progress operation.") + return ctrl.Result{}, err + } else if result.IsZero() { + // NOTE: if reconcileOperation completes successfully, then it will return a "zero" result, + // this means the operation has completed and we should continue processing. + logger.Info("Operation completed successfully.") + } else { + logger.Info("Requeueing to continue operation.") + return result, nil + } + } + + if DeploymentResource.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &DeploymentResource) + } + + // Nothing to do here, continue processing + return ctrl.Result{}, nil +} + +// reconcileOperation reconciles a DeploymentResource that has an operation in progress. +func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx) + + if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { + poller, err := r.Radius.Resources(deploymentResource.Status.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) + } + + _, err = poller.Poll(ctx) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to poll operation status: %w", err) + } + + if !poller.Done() { + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here, the operation is complete. + _, err = poller.Result(ctx) + if err != nil { + // Operation failed, reset state and retry. + r.EventRecorder.Event(deploymentResource, corev1.EventTypeWarning, "ResourceError", err.Error()) + logger.Error(err, "Delete failed.") + + deploymentResource.Status.Operation = nil + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseFailed + deploymentResource.Status.Message = err.Error() + + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here, the operation was a success. Update the status and continue. + // + // NOTE: we don't need to save the status here, because we're going to continue reconciling. + deploymentResource.Status.Operation = nil + return ctrl.Result{}, nil + } + + // If we get here, this was an unknown operation kind. This is a bug in our code, or someone + // tampered with the status of the object. Just reset the state and move on. + logger.Error(fmt.Errorf("unknown operation kind: %s", deploymentResource.Status.Operation.OperationKind), "Unknown operation kind.") + + deploymentResource.Status.Operation = nil + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseFailed + + err := r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx) + + // Since we're going to reconcile, update the observed generation. + // + // We don't want to do this if we're in the middle of an operation, because we haven't + // fully processed any status changes until the async operation completes. + deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + + poller, err := r.startDeleteOperation(ctx, deploymentResource) + if err != nil { + logger.Error(err, "Unable to delete resource.") + r.EventRecorder.Event(deploymentResource, corev1.EventTypeWarning, "ResourceError", err.Error()) + return ctrl.Result{}, err + } else if poller != nil { + // We've successfully started an operation. Update the status and requeue. + token, err := poller.ResumeToken() + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) + } + + deploymentResource.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + logger.Info("Resource is deleted.") + + // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the + // DeploymentResource + if controllerutil.RemoveFinalizer(deploymentResource, DeploymentResourceFinalizer) { + err := r.Client.Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + } + + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + r.EventRecorder.Event(deploymentResource, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") + return ctrl.Result{}, nil +} + +func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[generated.GenericResourcesClientDeleteResponse], error) { + logger := ucplog.FromContextOrDiscard(ctx) + + resourceId := deploymentResource.Spec.ResourceId + + logger.Info("Starting DELETE operation.") + poller, err := deleteResource(ctx, r.Radius, resourceId) + if err != nil { + return nil, err + } else if poller != nil { + return poller, nil + } + + // Deletion was synchronous + return nil, nil +} + +func (r *DeploymentResourceReconciler) requeueDelay() time.Duration { + delay := r.DelayInterval + if delay == 0 { + delay = PollingDelay + } + + return delay +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeploymentResourceReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&radappiov1alpha3.DeploymentResource{}). + Complete(r) +} diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go new file mode 100644 index 0000000000..8ab4408abb --- /dev/null +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -0,0 +1,522 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/go-logr/logr" + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/pkg/ucp/ucplog" + corev1 "k8s.io/api/core/v1" +) + +const ( + deploymentResourceType = "Microsoft.Resources/deployments" + + // TODOWILLSMITH: hardcoded, how do we get this? + RadiusResourceGroup = "default" +) + +// DeploymentTemplateReconciler reconciles a DeploymentTemplate object. +type DeploymentTemplateReconciler struct { + // Client is the Kubernetes client. + Client client.Client + + // Scheme is the Kubernetes scheme. + Scheme *runtime.Scheme + + // EventRecorder is the Kubernetes event recorder. + EventRecorder record.EventRecorder + + // Radius is the Radius client. + Radius RadiusClient + + // DelayInterval is the amount of time to wait between operations. + DelayInterval time.Duration +} + +// Reconcile is the main reconciliation loop for the DeploymentTemplate resource. +func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "DeploymentTemplate", "name", req.Name, "namespace", req.Namespace) + ctx = logr.NewContext(ctx, logger) + + deploymentTemplate := radappiov1alpha3.DeploymentTemplate{} + err := r.Client.Get(ctx, req.NamespacedName, &deploymentTemplate) + if apierrors.IsNotFound(err) { + // This can happen due to a data-race if the Deployment Template is created and then deleted before we can + // reconcile it. There's nothing to do here. + logger.Info("DeploymentTemplate is being deleted.") + return ctrl.Result{}, nil + } else if err != nil { + logger.Error(err, "Unable to fetch resource.") + return ctrl.Result{}, err + } + + // Our algorithm is as follows: + // + // TODOWILLSMITH: put algorithm here + // + // We do it this way because it guarantees that we only have one operation going at a time. + + if deploymentTemplate.Status.Operation != nil { + result, err := r.reconcileOperation(ctx, &deploymentTemplate) + if err != nil { + logger.Error(err, "Unable to reconcile in-progress operation.") + return ctrl.Result{}, err + } else if result.IsZero() { + // NOTE: if reconcileOperation completes successfully, then it will return a "zero" result, + // this means the operation has completed and we should continue processing. + logger.Info("Operation completed successfully.") + } else { + logger.Info("Requeueing to continue operation.") + return result, nil + } + } + + if deploymentTemplate.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &deploymentTemplate) + } + + return r.reconcileUpdate(ctx, &deploymentTemplate) +} + +// reconcileOperation reconciles a DeploymentTemplate that has an operation in progress. +func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx) + + if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { + poller, err := r.Radius.Resources(deploymentTemplate.Status.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) + } + + _, err = poller.Poll(ctx) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to poll operation status: %w", err) + } + + if !poller.Done() { + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here, the operation is complete. + resp, err := poller.Result(ctx) + if err != nil { + // Operation failed, reset state and retry. + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) + logger.Error(err, "Update failed.") + + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + deploymentTemplate.Status.Message = err.Error() + + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + //TODOWILLSMITH: clean this up + outputResources := make([]string, 0) + outputResourceList := resp.Properties["outputResources"].([]any) + for _, resource := range outputResourceList { + resource2 := resource.(map[string]any) + outputResources = append(outputResources, resource2["id"].(string)) + } + + // compare outputResources with existing DeploymentResources + // if is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + // if is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + // if is present in both, do nothing + + existingOutputResources := make(map[string]bool) + for _, resource := range deploymentTemplate.Status.OutputResources { + existingOutputResources[resource] = true + } + + for _, resource := range outputResources { + if _, ok := existingOutputResources[resource]; !ok { + // resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + + resourceName := generateDeploymentResourceName(resource) + deploymentResource := &radappiov1alpha3.DeploymentResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: deploymentTemplate.Namespace, + }, + Spec: radappiov1alpha3.DeploymentResourceSpec{ + ResourceId: resource, + }, + } + + if controllerutil.AddFinalizer(deploymentResource, DeploymentResourceFinalizer) { + if err := controllerutil.SetControllerReference(deploymentTemplate, deploymentResource, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + err = r.Client.Create(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + } else { + // TODOWILLSMITH: what do we do here? + } + } + } + + for _, resource := range deploymentTemplate.Status.OutputResources { + if _, ok := existingOutputResources[resource]; !ok { + // resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: resource, + Namespace: deploymentTemplate.Namespace, + }, + }) + if err != nil { + return ctrl.Result{}, err + } + } + } + + // If we get here, the operation was a success. Update the status and continue. + // + // NOTE: we don't need to save the status here, because we're going to continue reconciling. + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.OutputResources = outputResources + deploymentTemplate.Status.Resource = deploymentTemplate.Status.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + return ctrl.Result{}, nil + + } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { + poller, err := r.Radius.Resources(deploymentTemplate.Status.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) + } + + _, err = poller.Poll(ctx) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to poll operation status: %w", err) + } + + if !poller.Done() { + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here, the operation is complete. + _, err = poller.Result(ctx) + if err != nil { + // Operation failed, reset state and retry. + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) + logger.Error(err, "Delete failed.") + + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + deploymentTemplate.Status.Message = err.Error() + + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here, the operation was a success. Update the status and continue. + // + // NOTE: we don't need to save the status here, because we're going to continue reconciling. + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Resource = "" + return ctrl.Result{}, nil + } + + // If we get here, this was an unknown operation kind. This is a bug in our code, or someone + // tampered with the status of the object. Just reset the state and move on. + logger.Error(fmt.Errorf("unknown operation kind: %s", deploymentTemplate.Status.Operation.OperationKind), "Unknown operation kind.") + + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + + err := r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx) + + // Ensure that our finalizer is present before we start any operations. + if controllerutil.AddFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { + err := r.Client.Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + } + + // Since we're going to reconcile, update the observed generation. + // + // We don't want to do this if we're in the middle of an operation, because we haven't + // fully processed any status changes until the async operation completes. + deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + + // TODOWILLSMITH: Do some lookups to get the environment and application IDs. + // environmentName := "" + // applicationName := "" + + // resourceGroupID, environmentID, applicationID, err := resolveDependencies(ctx, r.Radius, "/planes/radius/local", environmentName, applicationName, labels) + // if err != nil { + // r.EventRecorder.Event(deployment, corev1.EventTypeWarning, "DependencyError", err.Error()) + // logger.Error(err, "Unable to resolve dependencies.") + // return ctrl.Result{}, fmt.Errorf("failed to resolve dependencies: %w", err) + // } + + // TODOWILLSMITH: This is a temporary workaround until we can get the correct resource group ID. + createDefaultResourceGroup(ctx, r.Radius) + deploymentTemplate.Status.Scope = "/planes/radius/local/resourcegroups/default" + + updatePoller, deletePoller, err := r.startPutOrDeleteOperationIfNeeded(ctx, deploymentTemplate) + if err != nil { + logger.Error(err, "Unable to create or update resource.") + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) + return ctrl.Result{}, err + } else if updatePoller != nil { + // We've successfully started an operation. Update the status and requeue. + token, err := updatePoller.ResumeToken() + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) + } + + deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindPut} + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseUpdating + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } else if deletePoller != nil { + // We've successfully started an operation. Update the status and requeue. + token, err := deletePoller.ResumeToken() + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) + } + + deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + // If we get here then it means we can process the result of the operation. + logger.Info("Resource is in desired state.", "resourceId", deploymentTemplate.Status.Resource) + + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseReady + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") + return ctrl.Result{}, nil +} + +func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { + logger := ucplog.FromContextOrDiscard(ctx) + + // Since we're going to reconcile, update the observed generation. + // + // We don't want to do this if we're in the middle of an operation, because we haven't + // fully processed any status changes until the async operation completes. + deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + + poller, err := r.startDeleteOperationIfNeeded(ctx, deploymentTemplate) + if err != nil { + logger.Error(err, "Unable to delete resource.") + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) + return ctrl.Result{}, err + } else if poller != nil { + // We've successfully started an operation. Update the status and requeue. + token, err := poller.ResumeToken() + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) + } + + deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + + logger.Info("Resource is deleted.") + + // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the + // DeploymentTemplate + if controllerutil.RemoveFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { + err := r.Client.Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + } + + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleted + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") + return ctrl.Result{}, nil +} + +func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], Poller[generated.GenericResourcesClientDeleteResponse], error) { + logger := ucplog.FromContextOrDiscard(ctx) + + resourceID := deploymentTemplate.Status.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + if deploymentTemplate.Status.Resource != "" && !strings.EqualFold(deploymentTemplate.Status.Resource, resourceID) { + // If we get here it means that the environment or application changed, so we should delete + // the old resource and create a new one. + // TODOWILLSMITH: ???? what is happening here + logger.Info("Resource is already created but is out-of-date") + + logger.Info("Starting DELETE operation.") + // poller, err := deleteResource(ctx, r.Radius, deploymentTemplate.Status.Resource) + // if err != nil { + // return nil, nil, err + // } else if poller != nil { + // return nil, poller, nil + // } + + // Deletion was synchronous + deploymentTemplate.Status.Resource = "" + } + + // Note: we separate this check from the previous block, because it could complete synchronously. + if deploymentTemplate.Status.Resource != "" { + logger.Info("Resource is already created and is up-to-date.") + return nil, nil, nil + } + + template := map[string]any{} + err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal template: %w", err) + } + + parameters := map[string]map[string]any{} + err = json.Unmarshal([]byte(deploymentTemplate.Spec.Parameters), ¶meters) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal parameters: %w", err) + } + + logger.Info("Starting PUT operation.") + properties := map[string]any{ + "mode": "Incremental", + "providerConfig": map[string]any{ + "deployments": map[string]any{ + "type": "Microsoft.Resources", + "value": map[string]any{ + "scope": deploymentTemplate.Status.Scope, + }, + }, + "radius": map[string]any{ + "type": "Radius", + "value": map[string]any{ + "scope": deploymentTemplate.Status.Scope, + }, + }, + }, // TODOWILLSMITH: other providers (az, aws) get from env? + "template": template, + "parameters": parameters, + } + + poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) + if err != nil { + return nil, nil, err + } else if poller != nil { + return poller, nil, nil + } + + // Update was synchronous + deploymentTemplate.Status.Resource = resourceID + return nil, nil, nil +} + +func (r *DeploymentTemplateReconciler) startDeleteOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientDeleteResponse], error) { + logger := ucplog.FromContextOrDiscard(ctx) + if deploymentTemplate.Status.Resource == "" { + logger.Info("Resource is already deleted (or was never created).") + return nil, nil + } + + logger.Info("Starting DELETE operation.") + // poller, err := deleteResource(ctx, r.Radius, deploymentTemplate.Status.Resource) + // if err != nil { + // return nil, err + // } else if poller != nil { + // return poller, err + // } + + // Deletion was synchronous + + deploymentTemplate.Status.Resource = "" + return nil, nil +} + +func (r *DeploymentTemplateReconciler) requeueDelay() time.Duration { + delay := r.DelayInterval + if delay == 0 { + delay = PollingDelay + } + + return delay +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DeploymentTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&radappiov1alpha3.DeploymentTemplate{}). + Owns(&radappiov1alpha3.DeploymentResource{}). + Complete(r) +} diff --git a/pkg/controller/reconciler/util.go b/pkg/controller/reconciler/util.go index 0ce88e7cfa..dfec5d2769 100644 --- a/pkg/controller/reconciler/util.go +++ b/pkg/controller/reconciler/util.go @@ -280,3 +280,19 @@ func createOrUpdateContainer(ctx context.Context, radius RadiusClient, container return nil, nil } + +func createDefaultResourceGroup(ctx context.Context, radius RadiusClient) { + // NOTE: using resource groups with lowercase here is a workaround for a casing bug in `rad app graph`. + // When https://github.com/radius-project/radius/issues/6422 is fixed we can use the more correct casing. + resourceGroupID := "/planes/radius/local/resourcegroups/default" + err := createResourceGroupIfNotExists(ctx, radius, resourceGroupID) + if err != nil { + panic(err) + } +} + +func generateDeploymentResourceName(resourceId string) string { + resourceBaseName := strings.Split(resourceId, "/")[len(strings.Split(resourceId, "/"))-1] + + return resourceBaseName +} diff --git a/pkg/controller/service.go b/pkg/controller/service.go index 1c55ede895..2aaf3c4a0e 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -107,6 +107,24 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "Deployment", err) } + err = (&reconciler.DeploymentTemplateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: reconciler.NewClient(s.Options.UCPConnection), + }).SetupWithManager(mgr) + if err != nil { + return fmt.Errorf("failed to setup %s controller: %w", "DeploymentTemplate", err) + } + err = (&reconciler.DeploymentResourceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: reconciler.NewClient(s.Options.UCPConnection), + }).SetupWithManager(mgr) + if err != nil { + return fmt.Errorf("failed to setup %s controller: %w", "DeploymentResource", err) + } if s.TLSCertDir == "" { logger.Info("Webhooks will be skipped. TLS certificates not present.") From c3534bffbc6778c8ab2220ad7de39d33c43b5cdf Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 4 Oct 2024 15:11:04 -0700 Subject: [PATCH 02/65] deletion works Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymenttemplates.yaml | 18 +++- example/demo/demo.bicep | 31 +++++++ example/demo/demo.bicepparam | 4 + example/demo/demo.json | 74 +++++++++++++++ example/demo/demo.parameters.json | 12 +++ example/demo/demodeploymenttemplate.yaml | 91 +++++++++++++++++++ example/demo/demoenv.bicep | 23 +++++ example/demo/demoenv.bicepparam | 4 + example/demo/demoenv.json | 51 +++++++++++ example/demo/demoenvdeploymenttemplate.yaml | 65 +++++++++++++ example/{ => env}/env.bicep | 0 example/{ => env}/env.bicepparam | 0 example/{ => env}/env.json | 0 example/{ => env}/env.parameters.json | 0 .../envdeploymenttemplate.yaml} | 0 .../v1alpha3/deploymenttemplate_types.go | 13 ++- .../deploymenttemplate_reconciler.go | 77 +++++----------- pkg/controller/reconciler/util.go | 10 -- 18 files changed, 405 insertions(+), 68 deletions(-) create mode 100644 example/demo/demo.bicep create mode 100644 example/demo/demo.bicepparam create mode 100644 example/demo/demo.json create mode 100644 example/demo/demo.parameters.json create mode 100644 example/demo/demodeploymenttemplate.yaml create mode 100644 example/demo/demoenv.bicep create mode 100644 example/demo/demoenv.bicepparam create mode 100644 example/demo/demoenv.json create mode 100644 example/demo/demoenvdeploymenttemplate.yaml rename example/{ => env}/env.bicep (100%) rename example/{ => env}/env.bicepparam (100%) rename example/{ => env}/env.json (100%) rename example/{ => env}/env.parameters.json (100%) rename example/{deploymenttemplate.yaml => env/envdeploymenttemplate.yaml} (100%) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index 9d3c1fe987..c48e3729d3 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -43,12 +43,16 @@ spec: parameters: description: Parameters is the ARM JSON parameters for the template. type: string + scope: + description: Scope is the resource id of the Radius scope. + type: string template: description: Template is the ARM JSON manifest that defines the resources to deploy. type: string required: - parameters + - scope - template type: object status: @@ -82,16 +86,26 @@ spec: items: type: string type: array + parameters: + description: Parameters is the ARM JSON parameters for the template. + type: string phrase: description: Phrase indicates the current status of the Deployment Template. type: string resource: - description: Resource is the resource ID of the deployment. + description: Resource is the resource id of the deployment. type: string scope: - description: Scope is the resource ID of the scope. + description: Scope is the resource id of the Radius scope. + type: string + template: + description: Template is the ARM JSON manifest that defines the resources + to deploy. type: string + required: + - parameters + - template type: object type: object served: true diff --git a/example/demo/demo.bicep b/example/demo/demo.bicep new file mode 100644 index 0000000000..c786c845fc --- /dev/null +++ b/example/demo/demo.bicep @@ -0,0 +1,31 @@ +// Import the set of Radius resources (Applications.*) into Bicep +extension radius + +param port int +param tag string + +resource demoenv 'Applications.Core/environments@2023-10-01-preview' existing = { + name: 'demoenv' +} + +resource demoapp 'Applications.Core/applications@2023-10-01-preview' = { + name: 'demoapp' + properties: { + environment: demoenv.id + } +} + +resource democtnr 'Applications.Core/containers@2023-10-01-preview' = { + name: 'democtnr' + properties: { + application: demoapp.id + container: { + image: 'ghcr.io/radius-project/samples/demo:${tag}' + ports: { + web: { + containerPort: port + } + } + } + } +} diff --git a/example/demo/demo.bicepparam b/example/demo/demo.bicepparam new file mode 100644 index 0000000000..071a1d6483 --- /dev/null +++ b/example/demo/demo.bicepparam @@ -0,0 +1,4 @@ +using 'demo.bicep' + +param port = 3000 +param tag = 'latest' diff --git a/example/demo/demo.json b/example/demo/demo.json new file mode 100644 index 0000000000..18f6c656a8 --- /dev/null +++ b/example/demo/demo.json @@ -0,0 +1,74 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "12248912509408521812" + } + }, + "parameters": { + "port": { + "type": "int" + }, + "tag": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "demoenv": { + "existing": true, + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "demoenv" + } + }, + "demoapp": { + "import": "Radius", + "type": "Applications.Core/applications@2023-10-01-preview", + "properties": { + "name": "demoapp", + "properties": { + "environment": "[reference('demoenv').id]" + } + }, + "dependsOn": [ + "demoenv" + ] + }, + "democtnr": { + "import": "Radius", + "type": "Applications.Core/containers@2023-10-01-preview", + "properties": { + "name": "democtnr", + "properties": { + "application": "[reference('demoapp').id]", + "container": { + "image": "[format('ghcr.io/radius-project/samples/demo:{0}', parameters('tag'))]", + "ports": { + "web": { + "containerPort": "[parameters('port')]" + } + } + } + } + }, + "dependsOn": [ + "demoapp" + ] + } + } +} \ No newline at end of file diff --git a/example/demo/demo.parameters.json b/example/demo/demo.parameters.json new file mode 100644 index 0000000000..35b1aadfdd --- /dev/null +++ b/example/demo/demo.parameters.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "port": { + "value": 3000 + }, + "tag": { + "value": "latest" + } + } +} \ No newline at end of file diff --git a/example/demo/demodeploymenttemplate.yaml b/example/demo/demodeploymenttemplate.yaml new file mode 100644 index 0000000000..e1b3cee56b --- /dev/null +++ b/example/demo/demodeploymenttemplate.yaml @@ -0,0 +1,91 @@ +kind: DeploymentTemplate +apiVersion: radapp.io/v1alpha3 +metadata: + name: demo.bicep + namespace: radius-system +spec: + template: | + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "12248912509408521812" + } + }, + "parameters": { + "port": { + "type": "int" + }, + "tag": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "demoenv": { + "existing": true, + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "demoenv" + } + }, + "demoapp": { + "import": "Radius", + "type": "Applications.Core/applications@2023-10-01-preview", + "properties": { + "name": "demoapp", + "properties": { + "environment": "[reference('demoenv').id]" + } + }, + "dependsOn": [ + "demoenv" + ] + }, + "democtnr": { + "import": "Radius", + "type": "Applications.Core/containers@2023-10-01-preview", + "properties": { + "name": "democtnr", + "properties": { + "application": "[reference('demoapp').id]", + "container": { + "image": "[format('ghcr.io/radius-project/samples/demo:{0}', parameters('tag'))]", + "ports": { + "web": { + "containerPort": "[parameters('port')]" + } + } + } + } + }, + "dependsOn": [ + "demoapp" + ] + } + } + } + parameters: | + { + "port": { + "value": 3000 + }, + "tag": { + "value": "latest" + } + } + scope: "/planes/radius/local/resourcegroups/default" \ No newline at end of file diff --git a/example/demo/demoenv.bicep b/example/demo/demoenv.bicep new file mode 100644 index 0000000000..cd54cb8b32 --- /dev/null +++ b/example/demo/demoenv.bicep @@ -0,0 +1,23 @@ +// Import the set of Radius resources (Applications.*) into Bicep +extension radius + +param kubernetesNamespace string + +resource demoenv 'Applications.Core/environments@2023-10-01-preview' = { + name: 'demoenv' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: kubernetesNamespace + } + recipes: { + 'Applications.Datastores/redisCaches': { + default: { + templateKind: 'bicep' + templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest' + } + } + } + } +} diff --git a/example/demo/demoenv.bicepparam b/example/demo/demoenv.bicepparam new file mode 100644 index 0000000000..c772414102 --- /dev/null +++ b/example/demo/demoenv.bicepparam @@ -0,0 +1,4 @@ +using './demoenv.bicep' + +param kubernetesNamespace = 'default' + diff --git a/example/demo/demoenv.json b/example/demo/demoenv.json new file mode 100644 index 0000000000..f3534807a4 --- /dev/null +++ b/example/demo/demoenv.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "12233467588702176499" + } + }, + "parameters": { + "kubernetesNamespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "demoenv": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "demoenv", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('kubernetesNamespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "testrecipe": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/example/demo/demoenvdeploymenttemplate.yaml b/example/demo/demoenvdeploymenttemplate.yaml new file mode 100644 index 0000000000..25e9631b81 --- /dev/null +++ b/example/demo/demoenvdeploymenttemplate.yaml @@ -0,0 +1,65 @@ +kind: DeploymentTemplate +apiVersion: radapp.io/v1alpha3 +metadata: + name: demoenv.bicep + namespace: radius-system +spec: + template: | + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.30.23.60470", + "templateHash": "12233467588702176499" + } + }, + "parameters": { + "kubernetesNamespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "demoenv": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "demoenv", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('kubernetesNamespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" + } + } + } + } + } + } + } + } + parameters: | + { + "kubernetesNamespace": { + "value": "default" + } + } + scope: "/planes/radius/local/resourcegroups/default" \ No newline at end of file diff --git a/example/env.bicep b/example/env/env.bicep similarity index 100% rename from example/env.bicep rename to example/env/env.bicep diff --git a/example/env.bicepparam b/example/env/env.bicepparam similarity index 100% rename from example/env.bicepparam rename to example/env/env.bicepparam diff --git a/example/env.json b/example/env/env.json similarity index 100% rename from example/env.json rename to example/env/env.json diff --git a/example/env.parameters.json b/example/env/env.parameters.json similarity index 100% rename from example/env.parameters.json rename to example/env/env.parameters.json diff --git a/example/deploymenttemplate.yaml b/example/env/envdeploymenttemplate.yaml similarity index 100% rename from example/deploymenttemplate.yaml rename to example/env/envdeploymenttemplate.yaml diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 546fa7f247..c23a2ae4ef 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -27,6 +27,9 @@ type DeploymentTemplateSpec struct { // Parameters is the ARM JSON parameters for the template. Parameters string `json:"parameters"` + + // Scope is the resource id of the Radius scope. + Scope string `json:"scope"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -34,10 +37,16 @@ type DeploymentTemplateStatus struct { // ObservedGeneration is the most recent generation observed for this DeploymentTemplate. ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // Scope is the resource ID of the scope. + // Template is the ARM JSON manifest that defines the resources to deploy. + Template string `json:"template"` + + // Parameters is the ARM JSON parameters for the template. + Parameters string `json:"parameters"` + + // Scope is the resource id of the Radius scope. Scope string `json:"scope,omitempty"` - // Resource is the resource ID of the deployment. + // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` // OutputResources is a list of the resourceIds that were created by the template. diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 8ab4408abb..86c7aac580 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -40,9 +39,6 @@ import ( const ( deploymentResourceType = "Microsoft.Resources/deployments" - - // TODOWILLSMITH: hardcoded, how do we get this? - RadiusResourceGroup = "default" ) // DeploymentTemplateReconciler reconciles a DeploymentTemplate object. @@ -146,6 +142,8 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } + logger.Info("Creating output resources.") + //TODOWILLSMITH: clean this up outputResources := make([]string, 0) outputResourceList := resp.Properties["outputResources"].([]any) @@ -164,6 +162,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d existingOutputResources[resource] = true } + newOutputResources := make(map[string]bool) + for _, resource := range outputResources { + newOutputResources[resource] = true + } + for _, resource := range outputResources { if _, ok := existingOutputResources[resource]; !ok { // resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it @@ -188,18 +191,18 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d if err != nil { return ctrl.Result{}, err } - } else { - // TODOWILLSMITH: what do we do here? } } } for _, resource := range deploymentTemplate.Status.OutputResources { - if _, ok := existingOutputResources[resource]; !ok { + if _, ok := newOutputResources[resource]; !ok { // resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + logger.Info("Deleting resource.", "resourceId", resource) + resourceName := generateDeploymentResourceName(resource) err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ ObjectMeta: metav1.ObjectMeta{ - Name: resource, + Name: resourceName, Namespace: deploymentTemplate.Namespace, }, }) @@ -214,7 +217,9 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.OutputResources = outputResources - deploymentTemplate.Status.Resource = deploymentTemplate.Status.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template + deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters + deploymentTemplate.Status.Resource = deploymentTemplate.Spec.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { @@ -291,21 +296,6 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - // TODOWILLSMITH: Do some lookups to get the environment and application IDs. - // environmentName := "" - // applicationName := "" - - // resourceGroupID, environmentID, applicationID, err := resolveDependencies(ctx, r.Radius, "/planes/radius/local", environmentName, applicationName, labels) - // if err != nil { - // r.EventRecorder.Event(deployment, corev1.EventTypeWarning, "DependencyError", err.Error()) - // logger.Error(err, "Unable to resolve dependencies.") - // return ctrl.Result{}, fmt.Errorf("failed to resolve dependencies: %w", err) - // } - - // TODOWILLSMITH: This is a temporary workaround until we can get the correct resource group ID. - createDefaultResourceGroup(ctx, r.Radius) - deploymentTemplate.Status.Scope = "/planes/radius/local/resourcegroups/default" - updatePoller, deletePoller, err := r.startPutOrDeleteOperationIfNeeded(ctx, deploymentTemplate) if err != nil { logger.Error(err, "Unable to create or update resource.") @@ -379,6 +369,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting + deploymentTemplate.Status.Scope = deploymentTemplate.Spec.Scope err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -413,31 +404,14 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], Poller[generated.GenericResourcesClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) - resourceID := deploymentTemplate.Status.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name - if deploymentTemplate.Status.Resource != "" && !strings.EqualFold(deploymentTemplate.Status.Resource, resourceID) { - // If we get here it means that the environment or application changed, so we should delete - // the old resource and create a new one. - // TODOWILLSMITH: ???? what is happening here - logger.Info("Resource is already created but is out-of-date") - - logger.Info("Starting DELETE operation.") - // poller, err := deleteResource(ctx, r.Radius, deploymentTemplate.Status.Resource) - // if err != nil { - // return nil, nil, err - // } else if poller != nil { - // return nil, poller, nil - // } - - // Deletion was synchronous - deploymentTemplate.Status.Resource = "" - } - - // Note: we separate this check from the previous block, because it could complete synchronously. - if deploymentTemplate.Status.Resource != "" { + // If the resource is already created and is up-to-date, then we don't need to do anything. + if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && deploymentTemplate.Status.Parameters == deploymentTemplate.Spec.Parameters { logger.Info("Resource is already created and is up-to-date.") return nil, nil, nil } + logger.Info("Template or parameters have changed, starting PUT operation.") + template := map[string]any{} err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) if err != nil { @@ -457,13 +431,13 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con "deployments": map[string]any{ "type": "Microsoft.Resources", "value": map[string]any{ - "scope": deploymentTemplate.Status.Scope, + "scope": deploymentTemplate.Spec.Scope, }, }, "radius": map[string]any{ "type": "Radius", "value": map[string]any{ - "scope": deploymentTemplate.Status.Scope, + "scope": deploymentTemplate.Spec.Scope, }, }, }, // TODOWILLSMITH: other providers (az, aws) get from env? @@ -471,6 +445,7 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con "parameters": parameters, } + resourceID := deploymentTemplate.Spec.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) if err != nil { return nil, nil, err @@ -490,13 +465,7 @@ func (r *DeploymentTemplateReconciler) startDeleteOperationIfNeeded(ctx context. return nil, nil } - logger.Info("Starting DELETE operation.") - // poller, err := deleteResource(ctx, r.Radius, deploymentTemplate.Status.Resource) - // if err != nil { - // return nil, err - // } else if poller != nil { - // return poller, err - // } + // TODOWILLSMITH: do we need to do anything here? wait for DeploymentResources to be deleted? // Deletion was synchronous diff --git a/pkg/controller/reconciler/util.go b/pkg/controller/reconciler/util.go index dfec5d2769..9ff0aa63de 100644 --- a/pkg/controller/reconciler/util.go +++ b/pkg/controller/reconciler/util.go @@ -281,16 +281,6 @@ func createOrUpdateContainer(ctx context.Context, radius RadiusClient, container return nil, nil } -func createDefaultResourceGroup(ctx context.Context, radius RadiusClient) { - // NOTE: using resource groups with lowercase here is a workaround for a casing bug in `rad app graph`. - // When https://github.com/radius-project/radius/issues/6422 is fixed we can use the more correct casing. - resourceGroupID := "/planes/radius/local/resourcegroups/default" - err := createResourceGroupIfNotExists(ctx, radius, resourceGroupID) - if err != nil { - panic(err) - } -} - func generateDeploymentResourceName(resourceId string) string { resourceBaseName := strings.Split(resourceId, "/")[len(strings.Split(resourceId, "/"))-1] From af0d1bc8f6a0390499774901356ffeeb9013d0c9 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 10 Oct 2024 11:45:50 -0700 Subject: [PATCH 03/65] who watches the watcher Signed-off-by: willdavsmith --- .../templates/controller/deployment.yaml | 16 +++ deploy/images/controller/Dockerfile | 10 +- example/demo/demo.json | 31 +--- example/demo/demo.parameters.json | 12 -- example/demo/demodeploymenttemplate.yaml | 37 +---- example/demo/demoenv.json | 4 +- .../v1alpha3/deploymentresource_types.go | 9 +- .../v1alpha3/deploymenttemplate_types.go | 9 +- .../deploymentresource_reconciler.go | 4 +- .../deploymenttemplate_reconciler.go | 41 ++---- .../reconciler/gitrepository_predicate.go | 67 +++++++++ .../reconciler/gitrepository_watcher.go | 136 ++++++++++++++++++ pkg/sdk/clients/resourcedeploymentsclient.go | 2 +- 13 files changed, 259 insertions(+), 119 deletions(-) delete mode 100644 example/demo/demo.parameters.json create mode 100644 pkg/controller/reconciler/gitrepository_predicate.go create mode 100644 pkg/controller/reconciler/gitrepository_watcher.go diff --git a/deploy/Chart/templates/controller/deployment.yaml b/deploy/Chart/templates/controller/deployment.yaml index c055a7dc3f..f386850cf5 100644 --- a/deploy/Chart/templates/controller/deployment.yaml +++ b/deploy/Chart/templates/controller/deployment.yaml @@ -72,7 +72,23 @@ spec: mountPath: {{ .Values.global.rootCA.mountPath }} readOnly: true {{- end }} + - name: work-dir + mountPath: /work-dir + initContainers: + - name: bicep + image: "ghcr.io/willdavsmith/bicep:latest" + imagePullPolicy: 'Always' + # restartPolicy: Always + command: + - cp + - "/bicep" + - "/work-dir/bicep" + volumeMounts: + - name: work-dir + mountPath: "/work-dir" volumes: + - name: work-dir + emptyDir: {} - name: config-volume configMap: name: controller-config diff --git a/deploy/images/controller/Dockerfile b/deploy/images/controller/Dockerfile index 739b1d607e..21312cc3c8 100644 --- a/deploy/images/controller/Dockerfile +++ b/deploy/images/controller/Dockerfile @@ -1,5 +1,5 @@ # Use distroless image which already includes ca-certificates -FROM gcr.io/distroless/static:nonroot +FROM ubuntu # Argument for target architecture ARG TARGETARCH @@ -7,11 +7,17 @@ ARG TARGETARCH # Set the working directory WORKDIR / + +# Install libicu +RUN apt-get update && apt-get install -y libicu-dev && rm -rf /var/lib/apt/lists/* + # Copy the application binary for the specified architecture COPY ./linux_${TARGETARCH:-amd64}/release/controller / +COPY ./linux_${TARGETARCH:-amd64}/release/bicep-linux-x64 /bicep + # Set the user to non-root (65532:65532 is the default non-root user in distroless) -USER 65532:65532 +# USER 65532:65532 # Set the entrypoint to the application binary ENTRYPOINT ["/controller"] diff --git a/example/demo/demo.json b/example/demo/demo.json index 18f6c656a8..e8314b2cab 100644 --- a/example/demo/demo.json +++ b/example/demo/demo.json @@ -10,15 +10,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12248912509408521812" - } - }, - "parameters": { - "port": { - "type": "int" - }, - "tag": { - "type": "string" + "templateHash": "14905601654846494245" } }, "imports": { @@ -48,27 +40,6 @@ "dependsOn": [ "demoenv" ] - }, - "democtnr": { - "import": "Radius", - "type": "Applications.Core/containers@2023-10-01-preview", - "properties": { - "name": "democtnr", - "properties": { - "application": "[reference('demoapp').id]", - "container": { - "image": "[format('ghcr.io/radius-project/samples/demo:{0}', parameters('tag'))]", - "ports": { - "web": { - "containerPort": "[parameters('port')]" - } - } - } - } - }, - "dependsOn": [ - "demoapp" - ] } } } \ No newline at end of file diff --git a/example/demo/demo.parameters.json b/example/demo/demo.parameters.json deleted file mode 100644 index 35b1aadfdd..0000000000 --- a/example/demo/demo.parameters.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "port": { - "value": 3000 - }, - "tag": { - "value": "latest" - } - } -} \ No newline at end of file diff --git a/example/demo/demodeploymenttemplate.yaml b/example/demo/demodeploymenttemplate.yaml index e1b3cee56b..d5750e007f 100644 --- a/example/demo/demodeploymenttemplate.yaml +++ b/example/demo/demodeploymenttemplate.yaml @@ -17,15 +17,7 @@ spec: "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12248912509408521812" - } - }, - "parameters": { - "port": { - "type": "int" - }, - "tag": { - "type": "string" + "templateHash": "14905601654846494245" } }, "imports": { @@ -55,37 +47,10 @@ spec: "dependsOn": [ "demoenv" ] - }, - "democtnr": { - "import": "Radius", - "type": "Applications.Core/containers@2023-10-01-preview", - "properties": { - "name": "democtnr", - "properties": { - "application": "[reference('demoapp').id]", - "container": { - "image": "[format('ghcr.io/radius-project/samples/demo:{0}', parameters('tag'))]", - "ports": { - "web": { - "containerPort": "[parameters('port')]" - } - } - } - } - }, - "dependsOn": [ - "demoapp" - ] } } } parameters: | { - "port": { - "value": 3000 - }, - "tag": { - "value": "latest" - } } scope: "/planes/radius/local/resourcegroups/default" \ No newline at end of file diff --git a/example/demo/demoenv.json b/example/demo/demoenv.json index f3534807a4..7662addd2f 100644 --- a/example/demo/demoenv.json +++ b/example/demo/demoenv.json @@ -10,7 +10,7 @@ "_generator": { "name": "bicep", "version": "0.30.23.60470", - "templateHash": "12233467588702176499" + "templateHash": "10554523723058743346" } }, "parameters": { @@ -38,7 +38,7 @@ }, "recipes": { "Applications.Datastores/redisCaches": { - "testrecipe": { + "default": { "templateKind": "bicep", "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" } diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 6666a26143..4eef7152a0 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -17,19 +17,20 @@ limitations under the License. package v1alpha3 import ( + "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DeploymentResourceSpec defines the desired state of DeploymentResource type DeploymentResourceSpec struct { - // ResourceId is the Radius resource Id. - ResourceId string `json:"resourceId"` + // ID is the resource ID. + ID string `json:"id"` } // DeploymentResourceStatus defines the observed state of DeploymentResource type DeploymentResourceStatus struct { - // Scope is the resource ID of the scope. - Scope string `json:"scope,omitempty"` + // ProviderConfig specifies the scope for resources + ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index c23a2ae4ef..1293da3c66 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha3 import ( + "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -28,8 +29,8 @@ type DeploymentTemplateSpec struct { // Parameters is the ARM JSON parameters for the template. Parameters string `json:"parameters"` - // Scope is the resource id of the Radius scope. - Scope string `json:"scope"` + // ProviderConfig specifies the scope for resources + ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -43,8 +44,8 @@ type DeploymentTemplateStatus struct { // Parameters is the ARM JSON parameters for the template. Parameters string `json:"parameters"` - // Scope is the resource id of the Radius scope. - Scope string `json:"scope,omitempty"` + // ProviderConfig specifies the scope for resources + ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index e21f507dcd..9805700479 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -104,7 +104,7 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(deploymentResource.Status.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(deploymentResource.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -216,7 +216,7 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[generated.GenericResourcesClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) - resourceId := deploymentResource.Spec.ResourceId + resourceId := deploymentResource.Spec.ID logger.Info("Starting DELETE operation.") poller, err := deleteResource(ctx, r.Radius, resourceId) diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 86c7aac580..97b6d22d61 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -109,7 +109,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - poller, err := r.Radius.Resources(deploymentTemplate.Status.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -167,18 +167,18 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d newOutputResources[resource] = true } - for _, resource := range outputResources { - if _, ok := existingOutputResources[resource]; !ok { + for _, outputResourceId := range outputResources { + if _, ok := existingOutputResources[outputResourceId]; !ok { // resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it - resourceName := generateDeploymentResourceName(resource) + resourceName := generateDeploymentResourceName(outputResourceId) deploymentResource := &radappiov1alpha3.DeploymentResource{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: deploymentTemplate.Namespace, }, Spec: radappiov1alpha3.DeploymentResourceSpec{ - ResourceId: resource, + ID: outputResourceId, }, } @@ -219,11 +219,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters - deploymentTemplate.Status.Resource = deploymentTemplate.Spec.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + deploymentTemplate.Status.Resource = deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(deploymentTemplate.Status.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -369,7 +369,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - deploymentTemplate.Status.Scope = deploymentTemplate.Spec.Scope + deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope = deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -424,28 +424,17 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con return nil, nil, fmt.Errorf("failed to unmarshal parameters: %w", err) } + providerConfig := deploymentTemplate.Spec.ProviderConfig + logger.Info("Starting PUT operation.") properties := map[string]any{ - "mode": "Incremental", - "providerConfig": map[string]any{ - "deployments": map[string]any{ - "type": "Microsoft.Resources", - "value": map[string]any{ - "scope": deploymentTemplate.Spec.Scope, - }, - }, - "radius": map[string]any{ - "type": "Radius", - "value": map[string]any{ - "scope": deploymentTemplate.Spec.Scope, - }, - }, - }, // TODOWILLSMITH: other providers (az, aws) get from env? - "template": template, - "parameters": parameters, + "mode": "Incremental", + "providerConfig": providerConfig, + "template": template, + "parameters": parameters, } - resourceID := deploymentTemplate.Spec.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + resourceID := deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) if err != nil { return nil, nil, err diff --git a/pkg/controller/reconciler/gitrepository_predicate.go b/pkg/controller/reconciler/gitrepository_predicate.go new file mode 100644 index 0000000000..de2006e2ae --- /dev/null +++ b/pkg/controller/reconciler/gitrepository_predicate.go @@ -0,0 +1,67 @@ +/* +Copyright 2020, 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +// GitRepositoryRevisionChangePredicate triggers an update event +// when a GitRepository revision changes. +type GitRepositoryRevisionChangePredicate struct { + predicate.Funcs +} + +func (GitRepositoryRevisionChangePredicate) Create(e event.CreateEvent) bool { + src, ok := e.Object.(sourcev1.Source) + + if !ok || src.GetArtifact() == nil { + return false + } + + return true +} + +func (GitRepositoryRevisionChangePredicate) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil || e.ObjectNew == nil { + return false + } + + oldSource, ok := e.ObjectOld.(sourcev1.Source) + if !ok { + return false + } + + newSource, ok := e.ObjectNew.(sourcev1.Source) + if !ok { + return false + } + + if oldSource.GetArtifact() == nil && newSource.GetArtifact() != nil { + return true + } + + if oldSource.GetArtifact() != nil && newSource.GetArtifact() != nil && + oldSource.GetArtifact().Revision != newSource.GetArtifact().Revision { + return true + } + + return false +} diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go new file mode 100644 index 0000000000..9ecf6119bd --- /dev/null +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -0,0 +1,136 @@ +package reconciler + +import ( + "context" + "fmt" + "io/fs" + "os" + + "github.com/radius-project/radius/pkg/sdk/clients" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/pkg/http/fetch" + "github.com/fluxcd/pkg/tar" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" +) + +// GitRepositoryWatcher watches GitRepository objects for revision changes +type GitRepositoryWatcher struct { + client.Client + artifactFetcher *fetch.ArchiveFetcher + HttpRetry int +} + +func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { + r.artifactFetcher = fetch.New( + fetch.WithRetries(r.HttpRetry), + fetch.WithMaxDownloadSize(tar.UnlimitedUntarSize), + fetch.WithUntar(tar.WithMaxUntarSize(tar.UnlimitedUntarSize)), + // fetch.WithHostnameOverwrite(os.Getenv("SOURCE_CONTROLLER_LOCALHOST")), + fetch.WithLogger(nil), + ) + + return ctrl.NewControllerManagedBy(mgr). + For(&sourcev1.GitRepository{}, builder.WithPredicates(GitRepositoryRevisionChangePredicate{})). + Complete(r) +} + +// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch +// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories/status,verbs=get + +func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := ctrl.LoggerFrom(ctx) + + // get source object + var repository sourcev1.GitRepository + if err := r.Get(ctx, req.NamespacedName, &repository); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + artifact := repository.Status.Artifact + log.Info("New revision detected", "revision", artifact.Revision) + + // create tmp dir + tmpDir, err := os.MkdirTemp("", repository.Name) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to create temp dir, error: %w", err) + } + + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Error(err, "unable to remove temp dir") + } + }(tmpDir) + + // download and extract artifact + if err := r.artifactFetcher.Fetch(artifact.URL, artifact.Digest, tmpDir); err != nil { + log.Error(err, "unable to fetch artifact") + return ctrl.Result{}, err + } + + // list artifact content + files, err := os.ReadDir(tmpDir) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list files, error: %w", err) + } + + for _, f := range files { + r.processFile(ctx, f, tmpDir+"/") + } + + return ctrl.Result{}, nil +} + +func (r *GitRepositoryWatcher) processFile(ctx context.Context, f fs.DirEntry, path string) { + log := ctrl.LoggerFrom(ctx) + + if f.IsDir() { + log.Info("Processing Directory " + f.Name()) + files, err := os.ReadDir(path + f.Name()) + if err != nil { + log.Error(err, "failed to list files, error: %w", err) + } + + for _, f := range files { + r.processFile(ctx, f, path+f.Name()+"/") + } + } else { + log.Info("Processing File" + f.Name()) + template, parameters, providerConfig := r.processBicepFile(ctx, path+f.Name()) + deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: f.Name(), + Namespace: "radius-system", + }, + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: &providerConfig, + }, + } + + if err := r.Create(ctx, deploymentTemplate); err != nil { + log.Error(err, "unable to create deployment template") + } + + log.Info("Created Deployment Template", "name", deploymentTemplate.Name) + } +} + +func (r *GitRepositoryWatcher) processBicepFile(ctx context.Context, path string) (string, string, clients.ProviderConfig) { + log := ctrl.LoggerFrom(ctx) + + file, err := os.ReadFile(path) + if err != nil { + log.Error(err, "unable to read file") + return "", "", clients.NewDefaultProviderConfig("default") + } + + return string(file), "", clients.ProviderConfig{} +} diff --git a/pkg/sdk/clients/resourcedeploymentsclient.go b/pkg/sdk/clients/resourcedeploymentsclient.go index 11a3a2b72a..1cc67d363d 100644 --- a/pkg/sdk/clients/resourcedeploymentsclient.go +++ b/pkg/sdk/clients/resourcedeploymentsclient.go @@ -45,7 +45,7 @@ type DeploymentProperties struct { Template any `json:"template,omitempty"` // TemplateLink - The URI of the template. Use either the templateLink property or the template property, but not both. TemplateLink *armresources.TemplateLink `json:"templateLink,omitempty"` - //ProviderConfig specifies the scope for resources + // ProviderConfig specifies the scope for resources ProviderConfig any `json:"providerconfig,omitempty"` // Parameters - Name and value pairs that define the deployment parameters for the template. You use this element when you want to provide the parameter values directly in the request rather than link to an existing parameter file. Use either the parametersLink property or the parameters property, but not both. It can be a JObject or a well formed JSON string. Parameters any `json:"parameters,omitempty"` From d94f5057509199126ba67dfb67f8bb5f04bbf282 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 10 Oct 2024 12:19:47 -0700 Subject: [PATCH 04/65] controller-gen update Signed-off-by: willdavsmith --- bicep-types | 2 +- .../radius/radapp.io_deploymentresources.yaml | 12 +++++----- .../radius/radapp.io_deploymenttemplates.yaml | 13 +++++------ .../Chart/crds/radius/radapp.io_recipes.yaml | 2 +- go.mod | 8 +++++++ go.sum | 22 +++++++++++++++++++ .../v1alpha3/deploymentresource_types.go | 3 +-- .../v1alpha3/deploymenttemplate_types.go | 5 ++--- .../deploymentresource_reconciler.go | 2 +- .../deploymenttemplate_reconciler.go | 14 +++++++----- .../reconciler/gitrepository_watcher.go | 13 ++++++----- 11 files changed, 64 insertions(+), 32 deletions(-) diff --git a/bicep-types b/bicep-types index 0eb4785159..96b34cbb74 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 0eb478515986e790b522f136756c0406ad3b698a +Subproject commit 96b34cbb749f791c2b5b72f83d448568942aeb27 diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index f38cbecc31..5221f6e711 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: deploymentresources.radapp.io spec: group: radapp.io @@ -40,11 +40,11 @@ spec: spec: description: DeploymentResourceSpec defines the desired state of DeploymentResource properties: - resourceId: - description: ResourceId is the Radius resource Id. + id: + description: ID is the resource ID. type: string required: - - resourceId + - id type: object status: description: DeploymentResourceStatus defines the observed state of DeploymentResource @@ -75,8 +75,8 @@ spec: description: Phrase indicates the current status of the Deployment Resource. type: string - scope: - description: Scope is the resource ID of the scope. + providerConfig: + description: ProviderConfig specifies the scope for resources type: string type: object type: object diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index c48e3729d3..6897b37e15 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: deploymenttemplates.radapp.io spec: group: radapp.io @@ -43,8 +43,8 @@ spec: parameters: description: Parameters is the ARM JSON parameters for the template. type: string - scope: - description: Scope is the resource id of the Radius scope. + providerConfig: + description: ProviderConfig specifies the scope for resources type: string template: description: Template is the ARM JSON manifest that defines the resources @@ -52,7 +52,6 @@ spec: type: string required: - parameters - - scope - template type: object status: @@ -93,12 +92,12 @@ spec: description: Phrase indicates the current status of the Deployment Template. type: string + providerConfig: + description: ProviderConfig specifies the scope for resources + type: string resource: description: Resource is the resource id of the deployment. type: string - scope: - description: Scope is the resource id of the Radius scope. - type: string template: description: Template is the ARM JSON manifest that defines the resources to deploy. diff --git a/deploy/Chart/crds/radius/radapp.io_recipes.yaml b/deploy/Chart/crds/radius/radapp.io_recipes.yaml index fb2bf92974..039c7ff66e 100644 --- a/deploy/Chart/crds/radius/radapp.io_recipes.yaml +++ b/deploy/Chart/crds/radius/radapp.io_recipes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: recipes.radapp.io spec: group: radapp.io diff --git a/go.mod b/go.mod index 1265e03662..fa6e9b99b1 100644 --- a/go.mod +++ b/go.mod @@ -121,6 +121,11 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect + github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect + github.com/fluxcd/pkg/http/fetch v0.12.1 // indirect + github.com/fluxcd/pkg/tar v0.8.1 // indirect + github.com/fluxcd/source-controller/api v1.4.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect @@ -136,9 +141,11 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -152,6 +159,7 @@ require ( github.com/ulikunitz/xz v0.5.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zeebo/blake3 v0.2.3 // indirect go.mongodb.org/mongo-driver v1.15.1 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/tools v0.25.0 // indirect diff --git a/go.sum b/go.sum index 5deffccdc8..b50ee8fb39 100644 --- a/go.sum +++ b/go.sum @@ -464,6 +464,18 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/pkg/apis/acl v0.3.0 h1:UOrKkBTOJK+OlZX7n8rWt2rdBmDCoTK+f5TY2LcZi8A= +github.com/fluxcd/pkg/apis/acl v0.3.0/go.mod h1:WVF9XjSMVBZuU+HTTiSebGAWMgM7IYexFLyVWbK9bNY= +github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0= +github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280= +github.com/fluxcd/pkg/http/fetch v0.12.1 h1:Iap/cdKols3fW39/MyTGqNXHglaA1FJsWtFgYG2hbCQ= +github.com/fluxcd/pkg/http/fetch v0.12.1/go.mod h1:t3JL+uqJ46Wm0CwVRn6Pf/3kOqh45tMoR0pMxLhextQ= +github.com/fluxcd/pkg/tar v0.8.0 h1:YcEW7K40/XM8o+bkU23dceWtxdaKUpsKcsppLSp8QWc= +github.com/fluxcd/pkg/tar v0.8.0/go.mod h1:O0WUC+nUIw7Cnw1h/4V310kLvzW4tvacD/VZTJtGBUM= +github.com/fluxcd/pkg/tar v0.8.1 h1:K9RWV+E/+Qbz6Mzcg+S9DkVvZrWwJq4957Kqms183RQ= +github.com/fluxcd/pkg/tar v0.8.1/go.mod h1:vuGrnXQPcdi3M4DoVtwvAyvLnSeFgXRJckTGYuZOy2Q= +github.com/fluxcd/source-controller/api v1.4.1 h1:zV01D7xzHOXWbYXr36lXHWWYS7POARsjLt61Nbh3kVY= +github.com/fluxcd/source-controller/api v1.4.1/go.mod h1:gSjg57T+IG66SsBR0aquv+DFrm4YyBNpKIJVDnu3Ya8= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -769,6 +781,9 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -866,6 +881,8 @@ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 h1:LTxrNWOPwquJy9Cu3oz6QHJIO5M5gNyOZtSybXdyLA4= +github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1031,6 +1048,10 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= @@ -1321,6 +1342,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 4eef7152a0..635704c2fb 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha3 import ( - "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -30,7 +29,7 @@ type DeploymentResourceSpec struct { // DeploymentResourceStatus defines the observed state of DeploymentResource type DeploymentResourceStatus struct { // ProviderConfig specifies the scope for resources - ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` + ProviderConfig string `json:"providerConfig,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 1293da3c66..2fcad40171 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha3 import ( - "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -30,7 +29,7 @@ type DeploymentTemplateSpec struct { Parameters string `json:"parameters"` // ProviderConfig specifies the scope for resources - ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` + ProviderConfig string `json:"providerConfig,omitempty"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -45,7 +44,7 @@ type DeploymentTemplateStatus struct { Parameters string `json:"parameters"` // ProviderConfig specifies the scope for resources - ProviderConfig *clients.ProviderConfig `json:"providerConfig,omitempty"` + ProviderConfig string `json:"providerConfig,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 9805700479..342c6c1bdb 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -104,7 +104,7 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(deploymentResource.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 97b6d22d61..4152dd0583 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -37,6 +37,10 @@ import ( corev1 "k8s.io/api/core/v1" ) +const ( + TEMPDEFAULTRADIUSRESOURCEGROUP = "/planes/radius/local/resourcegroups/default" +) + const ( deploymentResourceType = "Microsoft.Resources/deployments" ) @@ -109,7 +113,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -219,11 +223,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters - deploymentTemplate.Status.Resource = deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + deploymentTemplate.Status.Resource = TEMPDEFAULTRADIUSRESOURCEGROUP + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -369,7 +373,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope = deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope + deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -434,7 +438,7 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con "parameters": parameters, } - resourceID := deploymentTemplate.Status.ProviderConfig.Radius.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + resourceID := TEMPDEFAULTRADIUSRESOURCEGROUP + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) if err != nil { return nil, nil, err diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go index 9ecf6119bd..34ccb0501f 100644 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -6,7 +6,6 @@ import ( "io/fs" "os" - "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -111,7 +110,7 @@ func (r *GitRepositoryWatcher) processFile(ctx context.Context, f fs.DirEntry, p Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: template, Parameters: parameters, - ProviderConfig: &providerConfig, + ProviderConfig: providerConfig, }, } @@ -123,14 +122,16 @@ func (r *GitRepositoryWatcher) processFile(ctx context.Context, f fs.DirEntry, p } } -func (r *GitRepositoryWatcher) processBicepFile(ctx context.Context, path string) (string, string, clients.ProviderConfig) { +func (r *GitRepositoryWatcher) processBicepFile(ctx context.Context, path string) (string, string, string) { log := ctrl.LoggerFrom(ctx) - file, err := os.ReadFile(path) + _, err := os.ReadFile(path) if err != nil { log.Error(err, "unable to read file") - return "", "", clients.NewDefaultProviderConfig("default") + return "", "", "" } - return string(file), "", clients.ProviderConfig{} + // TODOWILLSMITH: compilebicep + + return "", "", "" } From 5a4739680d54a7bc3f0ef2eaa7d367f51e947e23 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 17 Oct 2024 08:45:28 -0700 Subject: [PATCH 05/65] PR Signed-off-by: willdavsmith --- deploy/Chart/templates/controller/rbac.yaml | 14 + deploy/images/controller/Dockerfile | 6 - example/demo/demo.bicep | 31 -- example/demo/demo.bicepparam | 4 - example/demo/demo.json | 45 --- example/demo/demodeploymenttemplate.yaml | 56 --- example/demo/demoenv.bicep | 23 -- example/demo/demoenv.bicepparam | 4 - example/demo/demoenv.json | 51 --- example/demo/demoenvdeploymenttemplate.yaml | 65 --- example/env/env.bicep | 24 -- example/env/env.bicepparam | 3 - example/env/env.json | 52 --- example/env/env.parameters.json | 9 - example/env/envdeploymenttemplate.yaml | 65 --- go.mod | 14 +- go.sum | 20 +- pkg/cli/deployment/deploy.go | 1 - .../v1alpha3/deploymentresource_types.go | 15 +- .../v1alpha3/deploymenttemplate_types.go | 17 +- pkg/controller/reconciler/const.go | 6 + .../deploymentresource_reconciler.go | 12 +- .../deploymentresource_reconciler_test.go | 179 +++++++++ .../deploymenttemplate_reconciler.go | 59 ++- .../deploymenttemplate_reconciler_test.go | 375 ++++++++++++++++++ .../reconciler/gitrepository_predicate.go | 2 +- .../gitrepository_predicate_test.go | 43 ++ .../reconciler/gitrepository_watcher.go | 137 +++++-- .../reconciler/gitrepository_watcher_test.go | 26 ++ pkg/controller/reconciler/shared_test.go | 30 ++ pkg/controller/service.go | 10 + 31 files changed, 886 insertions(+), 512 deletions(-) delete mode 100644 example/demo/demo.bicep delete mode 100644 example/demo/demo.bicepparam delete mode 100644 example/demo/demo.json delete mode 100644 example/demo/demodeploymenttemplate.yaml delete mode 100644 example/demo/demoenv.bicep delete mode 100644 example/demo/demoenv.bicepparam delete mode 100644 example/demo/demoenv.json delete mode 100644 example/demo/demoenvdeploymenttemplate.yaml delete mode 100644 example/env/env.bicep delete mode 100644 example/env/env.bicepparam delete mode 100644 example/env/env.json delete mode 100644 example/env/env.parameters.json delete mode 100644 example/env/envdeploymenttemplate.yaml create mode 100644 pkg/controller/reconciler/deploymentresource_reconciler_test.go create mode 100644 pkg/controller/reconciler/deploymenttemplate_reconciler_test.go create mode 100644 pkg/controller/reconciler/gitrepository_predicate_test.go create mode 100644 pkg/controller/reconciler/gitrepository_watcher_test.go diff --git a/deploy/Chart/templates/controller/rbac.yaml b/deploy/Chart/templates/controller/rbac.yaml index db554aa8d5..0a6870ad13 100644 --- a/deploy/Chart/templates/controller/rbac.yaml +++ b/deploy/Chart/templates/controller/rbac.yaml @@ -56,6 +56,20 @@ rules: - '*' verbs: - '*' +- apiGroups: + - source.toolkit.fluxcd.io + resources: + - gitrepositories + verbs: + - get + - list + - watch +- apiGroups: + - source.toolkit.fluxcd.io + resources: + - gitrepositories/status + verbs: + - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/deploy/images/controller/Dockerfile b/deploy/images/controller/Dockerfile index 21312cc3c8..f459dfe358 100644 --- a/deploy/images/controller/Dockerfile +++ b/deploy/images/controller/Dockerfile @@ -7,15 +7,9 @@ ARG TARGETARCH # Set the working directory WORKDIR / - -# Install libicu -RUN apt-get update && apt-get install -y libicu-dev && rm -rf /var/lib/apt/lists/* - # Copy the application binary for the specified architecture COPY ./linux_${TARGETARCH:-amd64}/release/controller / -COPY ./linux_${TARGETARCH:-amd64}/release/bicep-linux-x64 /bicep - # Set the user to non-root (65532:65532 is the default non-root user in distroless) # USER 65532:65532 diff --git a/example/demo/demo.bicep b/example/demo/demo.bicep deleted file mode 100644 index c786c845fc..0000000000 --- a/example/demo/demo.bicep +++ /dev/null @@ -1,31 +0,0 @@ -// Import the set of Radius resources (Applications.*) into Bicep -extension radius - -param port int -param tag string - -resource demoenv 'Applications.Core/environments@2023-10-01-preview' existing = { - name: 'demoenv' -} - -resource demoapp 'Applications.Core/applications@2023-10-01-preview' = { - name: 'demoapp' - properties: { - environment: demoenv.id - } -} - -resource democtnr 'Applications.Core/containers@2023-10-01-preview' = { - name: 'democtnr' - properties: { - application: demoapp.id - container: { - image: 'ghcr.io/radius-project/samples/demo:${tag}' - ports: { - web: { - containerPort: port - } - } - } - } -} diff --git a/example/demo/demo.bicepparam b/example/demo/demo.bicepparam deleted file mode 100644 index 071a1d6483..0000000000 --- a/example/demo/demo.bicepparam +++ /dev/null @@ -1,4 +0,0 @@ -using 'demo.bicep' - -param port = 3000 -param tag = 'latest' diff --git a/example/demo/demo.json b/example/demo/demo.json deleted file mode 100644 index e8314b2cab..0000000000 --- a/example/demo/demo.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.1-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14905601654846494245" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "demoenv": { - "existing": true, - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "demoenv" - } - }, - "demoapp": { - "import": "Radius", - "type": "Applications.Core/applications@2023-10-01-preview", - "properties": { - "name": "demoapp", - "properties": { - "environment": "[reference('demoenv').id]" - } - }, - "dependsOn": [ - "demoenv" - ] - } - } -} \ No newline at end of file diff --git a/example/demo/demodeploymenttemplate.yaml b/example/demo/demodeploymenttemplate.yaml deleted file mode 100644 index d5750e007f..0000000000 --- a/example/demo/demodeploymenttemplate.yaml +++ /dev/null @@ -1,56 +0,0 @@ -kind: DeploymentTemplate -apiVersion: radapp.io/v1alpha3 -metadata: - name: demo.bicep - namespace: radius-system -spec: - template: | - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.1-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "14905601654846494245" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "demoenv": { - "existing": true, - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "demoenv" - } - }, - "demoapp": { - "import": "Radius", - "type": "Applications.Core/applications@2023-10-01-preview", - "properties": { - "name": "demoapp", - "properties": { - "environment": "[reference('demoenv').id]" - } - }, - "dependsOn": [ - "demoenv" - ] - } - } - } - parameters: | - { - } - scope: "/planes/radius/local/resourcegroups/default" \ No newline at end of file diff --git a/example/demo/demoenv.bicep b/example/demo/demoenv.bicep deleted file mode 100644 index cd54cb8b32..0000000000 --- a/example/demo/demoenv.bicep +++ /dev/null @@ -1,23 +0,0 @@ -// Import the set of Radius resources (Applications.*) into Bicep -extension radius - -param kubernetesNamespace string - -resource demoenv 'Applications.Core/environments@2023-10-01-preview' = { - name: 'demoenv' - properties: { - compute: { - kind: 'kubernetes' - resourceId: 'self' - namespace: kubernetesNamespace - } - recipes: { - 'Applications.Datastores/redisCaches': { - default: { - templateKind: 'bicep' - templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest' - } - } - } - } -} diff --git a/example/demo/demoenv.bicepparam b/example/demo/demoenv.bicepparam deleted file mode 100644 index c772414102..0000000000 --- a/example/demo/demoenv.bicepparam +++ /dev/null @@ -1,4 +0,0 @@ -using './demoenv.bicep' - -param kubernetesNamespace = 'default' - diff --git a/example/demo/demoenv.json b/example/demo/demoenv.json deleted file mode 100644 index 7662addd2f..0000000000 --- a/example/demo/demoenv.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.1-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "10554523723058743346" - } - }, - "parameters": { - "kubernetesNamespace": { - "type": "string" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "demoenv": { - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "demoenv", - "properties": { - "compute": { - "kind": "kubernetes", - "resourceId": "self", - "namespace": "[parameters('kubernetesNamespace')]" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/example/demo/demoenvdeploymenttemplate.yaml b/example/demo/demoenvdeploymenttemplate.yaml deleted file mode 100644 index 25e9631b81..0000000000 --- a/example/demo/demoenvdeploymenttemplate.yaml +++ /dev/null @@ -1,65 +0,0 @@ -kind: DeploymentTemplate -apiVersion: radapp.io/v1alpha3 -metadata: - name: demoenv.bicep - namespace: radius-system -spec: - template: | - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "2.1-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "12233467588702176499" - } - }, - "parameters": { - "kubernetesNamespace": { - "type": "string" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "demoenv": { - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "demoenv", - "properties": { - "compute": { - "kind": "kubernetes", - "resourceId": "self", - "namespace": "[parameters('kubernetesNamespace')]" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" - } - } - } - } - } - } - } - } - parameters: | - { - "kubernetesNamespace": { - "value": "default" - } - } - scope: "/planes/radius/local/resourcegroups/default" \ No newline at end of file diff --git a/example/env/env.bicep b/example/env/env.bicep deleted file mode 100644 index a67fdb93b8..0000000000 --- a/example/env/env.bicep +++ /dev/null @@ -1,24 +0,0 @@ -// Import the set of Radius resources (Applications.*) into Bicep -extension radius - -param kubernetesNamespace string - -resource env 'Applications.Core/environments@2023-10-01-preview' = { - name: 'env' - location: 'global' - properties: { - compute: { - kind: 'kubernetes' - resourceId: 'self' - namespace: kubernetesNamespace - } - recipes: { - 'Applications.Datastores/redisCaches': { - testrecipe: { - templateKind: 'bicep' - templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36' - } - } - } - } -} diff --git a/example/env/env.bicepparam b/example/env/env.bicepparam deleted file mode 100644 index 999bbc731b..0000000000 --- a/example/env/env.bicepparam +++ /dev/null @@ -1,3 +0,0 @@ -using 'env.bicep' - -param kubernetesNamespace = 'default' diff --git a/example/env/env.json b/example/env/env.json deleted file mode 100644 index 77462fe66a..0000000000 --- a/example/env/env.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "1.9-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "9193032425594528343" - } - }, - "parameters": { - "kubernetesNamespace": { - "type": "string" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "env": { - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "env", - "location": "global", - "properties": { - "compute": { - "kind": "kubernetes", - "resourceId": "self", - "namespace": "[parameters('kubernetesNamespace')]" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "testrecipe": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36" - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/example/env/env.parameters.json b/example/env/env.parameters.json deleted file mode 100644 index 61e194d37e..0000000000 --- a/example/env/env.parameters.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "kubernetesNamespace": { - "value": "default" - } - } -} \ No newline at end of file diff --git a/example/env/envdeploymenttemplate.yaml b/example/env/envdeploymenttemplate.yaml deleted file mode 100644 index b95209df45..0000000000 --- a/example/env/envdeploymenttemplate.yaml +++ /dev/null @@ -1,65 +0,0 @@ -kind: DeploymentTemplate -apiVersion: radapp.io/v1alpha3 -metadata: - name: env.bicep - namespace: radius-system -spec: - template: | - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "languageVersion": "1.9-experimental", - "contentVersion": "1.0.0.0", - "metadata": { - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_generator": { - "name": "bicep", - "version": "0.30.23.60470", - "templateHash": "9193032425594528343" - } - }, - "parameters": { - "kubernetesNamespace": { - "type": "string" - } - }, - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "resources": { - "env": { - "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", - "properties": { - "name": "env", - "location": "global", - "properties": { - "compute": { - "kind": "kubernetes", - "resourceId": "self", - "namespace": "[parameters('kubernetesNamespace')]" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "testrecipe": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:0.36" - } - } - } - } - } - } - } - } - parameters: | - { - "kubernetesNamespace": { - "value": "default" - } - } diff --git a/go.mod b/go.mod index fa6e9b99b1..dab7a101cb 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module github.com/radius-project/radius go 1.23.0 +// Replace digest lib to master to gather access to BLAKE3. +// xref: https://github.com/opencontainers/go-digest/pull/66 +replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 @@ -30,7 +34,14 @@ require ( github.com/charmbracelet/x/ansi v0.4.5 github.com/charmbracelet/x/exp/teatest v0.0.0-20240408110044-525ba71bb562 github.com/dimchansky/utfbom v1.1.1 +<<<<<<< HEAD github.com/fatih/color v1.18.0 +======= + github.com/fatih/color v1.17.0 + github.com/fluxcd/pkg/http/fetch v0.12.1 + github.com/fluxcd/pkg/tar v0.8.1 + github.com/fluxcd/source-controller/api v1.4.1 +>>>>>>> 88eea07d2 (PR) github.com/go-chi/chi/v5 v5.1.0 github.com/go-git/go-git/v5 v5.12.0 github.com/go-logr/logr v1.4.2 @@ -123,9 +134,6 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect - github.com/fluxcd/pkg/http/fetch v0.12.1 // indirect - github.com/fluxcd/pkg/tar v0.8.1 // indirect - github.com/fluxcd/source-controller/api v1.4.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect diff --git a/go.sum b/go.sum index b50ee8fb39..dd9ebd5b7a 100644 --- a/go.sum +++ b/go.sum @@ -401,8 +401,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +<<<<<<< HEAD github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +======= +github.com/cyphar/filepath-securejoin v0.3.2 h1:QhZu5AxQ+o1XZH0Ye05YzvJ0kAdK6VQc0z9NNMek7gc= +github.com/cyphar/filepath-securejoin v0.3.2/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= +>>>>>>> 88eea07d2 (PR) github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -470,10 +475,10 @@ github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQ github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280= github.com/fluxcd/pkg/http/fetch v0.12.1 h1:Iap/cdKols3fW39/MyTGqNXHglaA1FJsWtFgYG2hbCQ= github.com/fluxcd/pkg/http/fetch v0.12.1/go.mod h1:t3JL+uqJ46Wm0CwVRn6Pf/3kOqh45tMoR0pMxLhextQ= -github.com/fluxcd/pkg/tar v0.8.0 h1:YcEW7K40/XM8o+bkU23dceWtxdaKUpsKcsppLSp8QWc= -github.com/fluxcd/pkg/tar v0.8.0/go.mod h1:O0WUC+nUIw7Cnw1h/4V310kLvzW4tvacD/VZTJtGBUM= github.com/fluxcd/pkg/tar v0.8.1 h1:K9RWV+E/+Qbz6Mzcg+S9DkVvZrWwJq4957Kqms183RQ= github.com/fluxcd/pkg/tar v0.8.1/go.mod h1:vuGrnXQPcdi3M4DoVtwvAyvLnSeFgXRJckTGYuZOy2Q= +github.com/fluxcd/pkg/testserver v0.7.0 h1:kNVAn+3bAF2rfR9cT6SxzgEz2o84i+o7zKY3XRKTXmk= +github.com/fluxcd/pkg/testserver v0.7.0/go.mod h1:Ih5IK3Y5G3+a6c77BTqFkdPDCY1Yj1A1W5cXQqkCs9s= github.com/fluxcd/source-controller/api v1.4.1 h1:zV01D7xzHOXWbYXr36lXHWWYS7POARsjLt61Nbh3kVY= github.com/fluxcd/source-controller/api v1.4.1/go.mod h1:gSjg57T+IG66SsBR0aquv+DFrm4YyBNpKIJVDnu3Ya8= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -875,12 +880,21 @@ github.com/novln/docker-parser v1.0.0/go.mod h1:oCeM32fsoUwkwByB5wVjsrsVQySzPWkl github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +<<<<<<< HEAD github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +======= +github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= +github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2PlhC9pm5sqpBZFvnAoKj+KzXRzbjFMA+TqXfJdgho= +github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +>>>>>>> 88eea07d2 (PR) github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 h1:LTxrNWOPwquJy9Cu3oz6QHJIO5M5gNyOZtSybXdyLA4= github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -1048,9 +1062,11 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= diff --git a/pkg/cli/deployment/deploy.go b/pkg/cli/deployment/deploy.go index 1c53e08d03..b00bd0557d 100644 --- a/pkg/cli/deployment/deploy.go +++ b/pkg/cli/deployment/deploy.go @@ -116,7 +116,6 @@ func (dc *ResourceDeploymentClient) startDeployment(ctx context.Context, name st resourceId = ucpresources.MakeUCPID(scopes, types, nil) providerConfig := dc.GetProviderConfigs(options) - //TODOWILLSMITH: reference poller, err := dc.Client.CreateOrUpdate(ctx, sdkclients.Deployment{ Properties: &sdkclients.DeploymentProperties{ diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 635704c2fb..6cd2c72969 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -1,5 +1,5 @@ /* -Copyright 2024. +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,22 +17,29 @@ limitations under the License. package v1alpha3 import ( + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DeploymentResourceSpec defines the desired state of DeploymentResource type DeploymentResourceSpec struct { // ID is the resource ID. - ID string `json:"id"` + ID string `json:"id,omitempty"` + + // ProviderConfig specifies the scope for resources + ProviderConfig string `json:"providerConfig,omitempty"` } // DeploymentResourceStatus defines the observed state of DeploymentResource type DeploymentResourceStatus struct { + // ID is the resource ID. + ID string `json:"id,omitempty"` + // ProviderConfig specifies the scope for resources - ProviderConfig string `json:"providerConfig,omitempty"` + ProviderConfig sdkclients.ProviderConfig `json:"providerConfig,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. - ObservedGeneration int64 `json:"observedGeneration,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` // Operation tracks the status of an in-progress provisioning operation. Operation *ResourceOperation `json:"operation,omitempty"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 2fcad40171..ced2a182c1 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -1,5 +1,5 @@ /* -Copyright 2024. +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,16 +17,17 @@ limitations under the License. package v1alpha3 import ( + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DeploymentTemplateSpec defines the desired state of DeploymentTemplate type DeploymentTemplateSpec struct { // Template is the ARM JSON manifest that defines the resources to deploy. - Template string `json:"template"` + Template string `json:"template,omitempty"` // Parameters is the ARM JSON parameters for the template. - Parameters string `json:"parameters"` + Parameters string `json:"parameters,omitempty"` // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` @@ -35,21 +36,21 @@ type DeploymentTemplateSpec struct { // DeploymentTemplateStatus defines the observed state of DeploymentTemplate type DeploymentTemplateStatus struct { // ObservedGeneration is the most recent generation observed for this DeploymentTemplate. - ObservedGeneration int64 `json:"observedGeneration,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` // Template is the ARM JSON manifest that defines the resources to deploy. - Template string `json:"template"` + Template string `json:"template,omitempty"` // Parameters is the ARM JSON parameters for the template. - Parameters string `json:"parameters"` + Parameters string `json:"parameters,omitempty"` // ProviderConfig specifies the scope for resources - ProviderConfig string `json:"providerConfig,omitempty"` + ProviderConfig sdkclients.ProviderConfig `json:"providerConfig,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` - // OutputResources is a list of the resourceIds that were created by the template. + // OutputResources is a list of the resourceIDs that were created by the template on the last deployment. OutputResources []string `json:"outputResources,omitempty"` // Operation tracks the status of an in-progress provisioning operation. diff --git a/pkg/controller/reconciler/const.go b/pkg/controller/reconciler/const.go index 77a39df103..bc82f4534f 100644 --- a/pkg/controller/reconciler/const.go +++ b/pkg/controller/reconciler/const.go @@ -53,4 +53,10 @@ const ( // DeploymentResourceFinalizer is the name of the finalizer added to DeploymentResources. DeploymentResourceFinalizer = "radapp.io/deployment-resource-finalizer" + + // RadiusSystemNamespace is the name of the system namespace where Radius resources are stored. + RadiusSystemNamespace = "radius-system" + + // GitRepositoryHttpRetryCount is the number of times to retry GitRepository HTTP requests. + GitRepositoryHttpRetryCount = 9 ) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 342c6c1bdb..25b9cbf418 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -1,5 +1,5 @@ /* -Copyright 2024. +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package reconciler import ( "context" + "encoding/json" "fmt" "time" @@ -31,6 +32,7 @@ import ( "github.com/go-logr/logr" "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" corev1 "k8s.io/api/core/v1" ) @@ -104,7 +106,13 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + providerConfig := sdkclients.ProviderConfig{} + err := json.Unmarshal([]byte(deploymentResource.Spec.ProviderConfig), &providerConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + } + + poller, err := r.Radius.Resources(providerConfig.Deployments.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go new file mode 100644 index 0000000000..062a080d6f --- /dev/null +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -0,0 +1,179 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "fmt" + "testing" + "time" + + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/test/testcontext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + DeploymentResourceTestWaitDuration = time.Second * 10 + DeploymentResourceTestWaitInterval = time.Second * 1 + DeploymentResourceTestControllerDelayInterval = time.Millisecond * 100 + + TestDeploymentResourceNamespace = "DeploymentResource-basic" + TestDeploymentResourceName = "test-DeploymentResource" + TestDeploymentResourceRadiusResourceGroup = "default-DeploymentResource-basic" +) + +var ( + TestDeploymentResourceScope = fmt.Sprintf("/planes/radius/local/resourcegroups/%s", TestDeploymentResourceRadiusResourceGroup) + TestDeploymentResourceID = fmt.Sprintf("%s/providers/Microsoft.Resources/deployments/%s", TestDeploymentResourceScope, TestDeploymentResourceName) +) + +func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client) { + SkipWithoutEnvironment(t) + + // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail + // because the logging will continue after the test completes. + // + // Add runtimelog "sigs.k8s.io/controller-runtime/pkg/log" to imports. + // + // runtimelog.SetLogger(ucplog.FromContextOrDiscard(testcontext.New(t))) + + // Shut down the manager when the test exits. + ctx, cancel := testcontext.NewWithCancel(t) + t.Cleanup(cancel) + + mgr, err := ctrl.NewManager(config, ctrl.Options{ + Scheme: scheme, + }) + require.NoError(t, err) + + radius := NewMockRadiusClient() + err = (&DeploymentResourceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("DeploymentResource-controller"), + Radius: radius, + DelayInterval: DeploymentResourceTestControllerDelayInterval, + }).SetupWithManager(mgr) + require.NoError(t, err) + + go func() { + err := mgr.Start(ctx) + require.NoError(t, err) + }() + + return radius, mgr.GetClient() +} + +func Test_DeploymentResourceReconciler_Basic(t *testing.T) { + ctx := testcontext.New(t) + radius, client := SetupDeploymentResourceTest(t) + + name := types.NamespacedName{Namespace: TestDeploymentResourceNamespace, Name: TestDeploymentResourceName} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + deployment := makeDeploymentResource(name, TestDeploymentResourceID) + err = client.Create(ctx, deployment) + require.NoError(t, err) + + // Deployment will update after operation completes + status := waitForDeploymentResourceStateReady(t, client, name) + require.Equal(t, TestDeploymentResourceID, status.ID) + + err = client.Delete(ctx, deployment) + require.NoError(t, err) + + // Deletion of the DeploymentResource is in progress. + status = waitForDeploymentResourceStateDeleting(t, client, name, nil) + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Now deleting of the DeploymentResource object can complete. + waitForDeploymentResourceDeleted(t, client, name) +} + +func waitForDeploymentResourceStateReady(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentResourceStatus { + ctx := testcontext.New(t) + + logger := t + status := &radappiov1alpha3.DeploymentResourceStatus{} + require.EventuallyWithTf(t, func(t *assert.CollectT) { + logger.Logf("Fetching DeploymentResource: %+v", name) + current := &radappiov1alpha3.DeploymentResource{} + err := client.Get(ctx, name, current) + require.NoError(t, err) + + status = ¤t.Status + logger.Logf("DeploymentResource.Status: %+v", current.Status) + assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") + + if assert.Equal(t, radappiov1alpha3.DeploymentResourcePhraseReady, current.Status.Phrase) { + assert.Empty(t, current.Status.Operation) + } + }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "failed to enter updating state") + + return status +} + +func waitForDeploymentResourceStateDeleting(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentResourceStatus { + ctx := testcontext.New(t) + + logger := t + status := &radappiov1alpha3.DeploymentResourceStatus{} + require.EventuallyWithTf(t, func(t *assert.CollectT) { + logger.Logf("Fetching DeploymentResource: %+v", name) + current := &radappiov1alpha3.DeploymentResource{} + err := client.Get(ctx, name, current) + assert.NoError(t, err) + + status = ¤t.Status + logger.Logf("DeploymentResource.Status: %+v", current.Status) + assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") + + if assert.Equal(t, radappiov1alpha3.DeploymentResourcePhraseDeleting, current.Status.Phrase) { + assert.NotEmpty(t, current.Status.Operation) + assert.NotEqual(t, oldOperation, current.Status.Operation) + } + }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "failed to enter deleting state") + + return status +} + +func waitForDeploymentResourceDeleted(t *testing.T, client client.Client, name types.NamespacedName) { + ctx := testcontext.New(t) + + logger := t + require.Eventuallyf(t, func() bool { + logger.Logf("Fetching DeploymentResource: %+v", name) + current := &radappiov1alpha3.DeploymentResource{} + err := client.Get(ctx, name, current) + if apierrors.IsNotFound(err) { + return true + } + + logger.Logf("DeploymentResource.Status: %+v", current.Status) + return false + + }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "DeploymentResource still exists") +} + +// TODOWILLSMITH: add more tests diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 4152dd0583..0f0cea75b1 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -1,5 +1,5 @@ /* -Copyright 2024. +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,14 +33,11 @@ import ( "github.com/go-logr/logr" "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" corev1 "k8s.io/api/core/v1" ) -const ( - TEMPDEFAULTRADIUSRESOURCEGROUP = "/planes/radius/local/resourcegroups/default" -) - const ( deploymentResourceType = "Microsoft.Resources/deployments" ) @@ -113,7 +110,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Deployments.Value.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -216,6 +213,12 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d } } + providerConfig := sdkclients.ProviderConfig{} + err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + } + // If we get here, the operation was a success. Update the status and continue. // // NOTE: we don't need to save the status here, because we're going to continue reconciling. @@ -223,11 +226,18 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters - deploymentTemplate.Status.Resource = TEMPDEFAULTRADIUSRESOURCEGROUP + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + deploymentTemplate.Status.ProviderConfig = providerConfig return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.Radius.Resources(TEMPDEFAULTRADIUSRESOURCEGROUP, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + providerConfig := sdkclients.ProviderConfig{} + err := json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + } + + poller, err := r.Radius.Resources(providerConfig.Deployments.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -371,9 +381,15 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) } + providerConfig := sdkclients.ProviderConfig{} + err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + } + deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig + deploymentTemplate.Status.ProviderConfig = providerConfig err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -416,19 +432,36 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con logger.Info("Template or parameters have changed, starting PUT operation.") - template := map[string]any{} + var template any err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) if err != nil { return nil, nil, fmt.Errorf("failed to unmarshal template: %w", err) } - parameters := map[string]map[string]any{} + var parameters any err = json.Unmarshal([]byte(deploymentTemplate.Spec.Parameters), ¶meters) if err != nil { return nil, nil, fmt.Errorf("failed to unmarshal parameters: %w", err) } - providerConfig := deploymentTemplate.Spec.ProviderConfig + // TODO PR: Is there a better way to check for all of this stuff? + providerConfig := sdkclients.ProviderConfig{} + err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal template: %w", err) + } + if providerConfig.Deployments == nil { + return nil, nil, fmt.Errorf("providerConfig.Deployments is nil") + } + if providerConfig.Deployments.Value.Scope == "" { + return nil, nil, fmt.Errorf("providerConfig.Deployments.Value.Scope is empty") + } + if providerConfig.Radius == nil { + return nil, nil, fmt.Errorf("providerConfig.Radius is nil") + } + if providerConfig.Radius.Value.Scope == "" { + return nil, nil, fmt.Errorf("providerConfig.Radius.Value.Scope is empty") + } logger.Info("Starting PUT operation.") properties := map[string]any{ @@ -438,7 +471,7 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con "parameters": parameters, } - resourceID := TEMPDEFAULTRADIUSRESOURCEGROUP + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) if err != nil { return nil, nil, err diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go new file mode 100644 index 0000000000..ab350b075e --- /dev/null +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -0,0 +1,375 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/test/testcontext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +//TODOWILLSMITH: finish this test + +const ( + DeploymentTemplateTestWaitDuration = time.Second * 10 + DeploymentTemplateTestWaitInterval = time.Second * 1 + DeploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 + + TestDeploymentTemplateNamespace = "DeploymentTemplate-basic" + TestDeploymentTemplateName = "test-DeploymentTemplate" + TestDeploymentTemplateRadiusResourceGroup = "default-DeploymentTemplate-basic" +) + +var ( + TestDeploymentTemplateScope = fmt.Sprintf("/planes/radius/local/resourcegroups/%s", TestDeploymentTemplateRadiusResourceGroup) + TestDeploymentTemplateID = fmt.Sprintf("%s/providers/Microsoft.Resources/deployments/%s", TestDeploymentTemplateScope, TestDeploymentTemplateName) +) + +func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client) { + SkipWithoutEnvironment(t) + + // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail + // because the logging will continue after the test completes. + // + // Add runtimelog "sigs.k8s.io/controller-runtime/pkg/log" to imports. + // + // runtimelog.SetLogger(ucplog.FromContextOrDiscard(testcontext.New(t))) + + // Shut down the manager when the test exits. + ctx, cancel := testcontext.NewWithCancel(t) + t.Cleanup(cancel) + + mgr, err := ctrl.NewManager(config, ctrl.Options{ + Scheme: scheme, + }) + require.NoError(t, err) + + radius := NewMockRadiusClient() + err = (&DeploymentTemplateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("DeploymentTemplate-controller"), + Radius: radius, + DelayInterval: DeploymentTemplateTestControllerDelayInterval, + }).SetupWithManager(mgr) + require.NoError(t, err) + + go func() { + err := mgr.Start(ctx) + require.NoError(t, err) + }() + + return radius, mgr.GetClient() +} + +func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { + ctx := testcontext.New(t) + radius, client := SetupDeploymentTemplateTest(t) + + name := types.NamespacedName{Namespace: "DeploymentTemplate-basic", Name: "test-DeploymentTemplate"} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + deployment := makeDeploymentTemplate(name, map[string]any{}) + err = client.Create(ctx, deployment) + require.NoError(t, err) + + // Deployment will be waiting for environment to be created. + createEnvironment(radius, "default") + + // Deployment will be waiting for template to complete provisioning. + status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", status.ProviderConfig.Deployments.Value.Scope) + + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Deployment will update after operation completes + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic/providers/Microsoft.Resources/deployments/test-DeploymentTemplate", status.Resource) + + resource, err := radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) + + expectedProperties := map[string]any{ + "mode": "Incremental", + "parameters": map[string]map[string]any{}, + "providerConfig": map[string]any{ + "deployments": map[string]any{ + "type": "Microsoft.Resources", + "value": map[string]any{ + "scope": "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", + }, + }, + "radius": map[string]any{ + "type": "Radius", + "value": map[string]any{ + "scope": "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", + }, + }, + }, "template": map[string]any{}, + } + require.Equal(t, expectedProperties, resource.Properties) + + err = client.Delete(ctx, deployment) + require.NoError(t, err) + + // Deletion of the DeploymentTemplate is in progress. + status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Now deleting of the DeploymentTemplate object can complete. + waitForDeploymentTemplateDeleted(t, client, name) +} + +func Test_DeploymentTemplateReconciler_ChangeEnvironmentAndApplication(t *testing.T) { + ctx := testcontext.New(t) + radius, client := SetupDeploymentTemplateTest(t) + + name := types.NamespacedName{Namespace: "DeploymentTemplate-change", Name: "test-DeploymentTemplate-change"} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + deployment := makeDeploymentTemplate(name, map[string]any{}) + err = client.Create(ctx, deployment) + require.NoError(t, err) + + // Deployment will be waiting for environment to be created. + createEnvironment(radius, "default") + + // Deployment will be waiting for template to complete provisioning. + status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-change", status.ProviderConfig.Deployments.Value.Scope) + + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Deployment will update after operation completes + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-change/providers/Microsoft.Resources/deployments/test-DeploymentTemplate-change", status.Resource) + + _, err = radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) + + createEnvironment(radius, "new-environment") + + // Now update the deployment to change the environment and application. + err = client.Get(ctx, name, deployment) + require.NoError(t, err) + + err = client.Update(ctx, deployment) + require.NoError(t, err) + + // Now the deployment will delete and re-create all of the items + + // Deletion of the deployment is in progress. + status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Resource should be gone. + _, err = radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.Error(t, err) + + // Deployment will be waiting for extender to complete provisioning. + status = waitForDeploymentTemplateStateUpdating(t, client, name, nil) + require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application", status.ProviderConfig.Deployments.Value.Scope) + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Deployment will update after operation completes + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application/providers/Microsoft.Resources/deployments/test-DeploymentTemplate-change", status.Resource) + + // Now delete the deployment. + err = client.Delete(ctx, deployment) + require.NoError(t, err) + + // Deletion of the resource is in progress. + status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Now deleting of the deployment object can complete. + waitForDeploymentTemplateDeleted(t, client, name) +} + +func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { + // This test tests our ability to recover from failed operations inside Radius. + // + // We use the mock client to simulate the failure of update and delete operations + // and verify that the controller will (eventually) retry these operations. + + ctx := testcontext.New(t) + radius, client := SetupDeploymentTemplateTest(t) + + name := types.NamespacedName{Namespace: "DeploymentTemplate-failure-recovery", Name: "test-DeploymentTemplate-failure-recovery"} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + deployment := makeDeploymentTemplate(name, map[string]any{}) + err = client.Create(ctx, deployment) + require.NoError(t, err) + + // Deployment will be waiting for environment to be created. + createEnvironment(radius, "default") + + // Deployment will be waiting for template to complete provisioning. + status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + + // Complete the operation, but make it fail. + operation := status.Operation + radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + state.err = errors.New("oops") + + resource, ok := radius.resources[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties["provisioningState"] = "Failed" + state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + }) + + // Deployment should (eventually) start a new provisioning operation + status = waitForDeploymentTemplateStateUpdating(t, client, name, operation) + + // Complete the operation, successfully this time. + radius.CompleteOperation(status.Operation.ResumeToken, nil) + _ = waitForDeploymentTemplateStateReady(t, client, name) + + err = client.Delete(ctx, deployment) + require.NoError(t, err) + + // Deletion of the deployment is in progress. + status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + + // Complete the operation, but make it fail. + operation = status.Operation + radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + state.err = errors.New("oops") + + resource, ok := radius.resources[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties["provisioningState"] = "Failed" + }) + + // Deployment should (eventually) start a new deletion operation + status = waitForDeploymentTemplateStateDeleting(t, client, name, operation) + + // Complete the operation, successfully this time. + radius.CompleteOperation(status.Operation.ResumeToken, nil) + + // Now deleting of the deployment object can complete. + waitForDeploymentTemplateDeleted(t, client, name) +} + +func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { + ctx := testcontext.New(t) + + logger := t + status := &radappiov1alpha3.DeploymentTemplateStatus{} + require.EventuallyWithT(t, func(t *assert.CollectT) { + logger.Logf("Fetching DeploymentTemplate: %+v", name) + current := &radappiov1alpha3.DeploymentTemplate{} + err := client.Get(ctx, name, current) + require.NoError(t, err) + + status = ¤t.Status + logger.Logf("DeploymentTemplate.Status: %+v", current.Status) + assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") + + if assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseUpdating, current.Status.Phrase) { + assert.NotEmpty(t, current.Status.Operation) + assert.NotEqual(t, oldOperation, current.Status.Operation) + } + + }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter updating state") + + return status +} + +func waitForDeploymentTemplateStateReady(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { + ctx := testcontext.New(t) + + logger := t + status := &radappiov1alpha3.DeploymentTemplateStatus{} + require.EventuallyWithTf(t, func(t *assert.CollectT) { + logger.Logf("Fetching DeploymentTemplate: %+v", name) + current := &radappiov1alpha3.DeploymentTemplate{} + err := client.Get(ctx, name, current) + require.NoError(t, err) + + status = ¤t.Status + logger.Logf("DeploymentTemplate.Status: %+v", current.Status) + assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") + + if assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseReady, current.Status.Phrase) { + assert.Empty(t, current.Status.Operation) + } + }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter updating state") + + return status +} + +func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { + ctx := testcontext.New(t) + + logger := t + status := &radappiov1alpha3.DeploymentTemplateStatus{} + require.EventuallyWithTf(t, func(t *assert.CollectT) { + logger.Logf("Fetching DeploymentTemplate: %+v", name) + current := &radappiov1alpha3.DeploymentTemplate{} + err := client.Get(ctx, name, current) + assert.NoError(t, err) + + status = ¤t.Status + logger.Logf("DeploymentTemplate.Status: %+v", current.Status) + assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") + + if assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseDeleting, current.Status.Phrase) { + assert.NotEmpty(t, current.Status.Operation) + assert.NotEqual(t, oldOperation, current.Status.Operation) + } + }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter deleting state") + + return status +} + +func waitForDeploymentTemplateDeleted(t *testing.T, client client.Client, name types.NamespacedName) { + ctx := testcontext.New(t) + + logger := t + require.Eventuallyf(t, func() bool { + logger.Logf("Fetching DeploymentTemplate: %+v", name) + current := &radappiov1alpha3.DeploymentTemplate{} + err := client.Get(ctx, name, current) + if apierrors.IsNotFound(err) { + return true + } + + logger.Logf("DeploymentTemplate.Status: %+v", current.Status) + return false + + }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "DeploymentTemplate still exists") +} diff --git a/pkg/controller/reconciler/gitrepository_predicate.go b/pkg/controller/reconciler/gitrepository_predicate.go index de2006e2ae..35e4b2fced 100644 --- a/pkg/controller/reconciler/gitrepository_predicate.go +++ b/pkg/controller/reconciler/gitrepository_predicate.go @@ -1,5 +1,5 @@ /* -Copyright 2020, 2021 The Flux authors +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pkg/controller/reconciler/gitrepository_predicate_test.go b/pkg/controller/reconciler/gitrepository_predicate_test.go new file mode 100644 index 0000000000..05fef6bee7 --- /dev/null +++ b/pkg/controller/reconciler/gitrepository_predicate_test.go @@ -0,0 +1,43 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "testing" + + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestGitRepositoryRevisionChangePredicate_Create(t *testing.T) { + //TODOWILLSMITH: finish this test + p := GitRepositoryRevisionChangePredicate{} + e := event.CreateEvent{} + + if p.Create(e) { + t.Errorf("expected false, got true") + } +} + +func TestGitRepositoryRevisionChangePredicate_Update(t *testing.T) { + //TODOWILLSMITH: finish this test + p := GitRepositoryRevisionChangePredicate{} + e := event.UpdateEvent{} + + if p.Update(e) { + t.Errorf("expected false, got true") + } +} diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go index 34ccb0501f..e362f6e524 100644 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -3,8 +3,10 @@ package reconciler import ( "context" "fmt" - "io/fs" "os" + "os/exec" + "path" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" @@ -30,7 +32,6 @@ func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { fetch.WithRetries(r.HttpRetry), fetch.WithMaxDownloadSize(tar.UnlimitedUntarSize), fetch.WithUntar(tar.WithMaxUntarSize(tar.UnlimitedUntarSize)), - // fetch.WithHostnameOverwrite(os.Getenv("SOURCE_CONTROLLER_LOCALHOST")), fetch.WithLogger(nil), ) @@ -45,7 +46,6 @@ func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := ctrl.LoggerFrom(ctx) - // get source object var repository sourcev1.GitRepository if err := r.Get(ctx, req.NamespacedName, &repository); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -67,7 +67,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) } }(tmpDir) - // download and extract artifact + log.Info("fetching artifact...", "url", artifact.URL) if err := r.artifactFetcher.Fetch(artifact.URL, artifact.Digest, tmpDir); err != nil { log.Error(err, "unable to fetch artifact") return ctrl.Result{}, err @@ -79,59 +79,116 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, fmt.Errorf("failed to list files, error: %w", err) } + // TODOWILLSMITH: how do we decide which files to run bicep build on? + // for now, we'll just run it on all root files for _, f := range files { - r.processFile(ctx, f, tmpDir+"/") + extension := path.Ext(f.Name()) + if extension == ".bicep" { + template, err := r.runBicepBuild(ctx, tmpDir, f.Name()) + if err != nil { + log.Error(err, "failed to run bicep build") + return ctrl.Result{}, err + } + + // TODOWILLSMITH: how do we decide which parameters file to use? + // for now, we assume the parameters file is the same name as the bicep file + // in the same directory + // e.g. main.bicep -> main.bicepparam + parametersFile := strings.ReplaceAll(f.Name(), ".bicep", ".bicepparam") + + parameters, err := r.runBicepBuildParams(ctx, tmpDir, parametersFile) + providerConfig := "providerConfig" + if err != nil { + log.Error(err, "failed to run bicep build-params") + return ctrl.Result{}, err + } + + // TODOWILLSMITH: create/update or delete + // determine if this bicep file has already been deployed, if so update + // if not, create, + // if the bicep file has been deleted, delete the deployment template + + // get all deployment templates on the cluster + // think ab multiple git repos scenario + // need to save name of git repo in deployment template? + + r.createOrUpdateDeploymentTemplate(ctx, f.Name(), template, parameters, providerConfig) + } } return ctrl.Result{}, nil } -func (r *GitRepositoryWatcher) processFile(ctx context.Context, f fs.DirEntry, path string) { +func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, filename string) (armJSON string, err error) { + // TODOWILLSMITH: bicep build is broken log := ctrl.LoggerFrom(ctx) - if f.IsDir() { - log.Info("Processing Directory " + f.Name()) - files, err := os.ReadDir(path + f.Name()) - if err != nil { - log.Error(err, "failed to list files, error: %w", err) - } - - for _, f := range files { - r.processFile(ctx, f, path+f.Name()+"/") - } - } else { - log.Info("Processing File" + f.Name()) - template, parameters, providerConfig := r.processBicepFile(ctx, path+f.Name()) - deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ - ObjectMeta: metav1.ObjectMeta{ - Name: f.Name(), - Namespace: "radius-system", - }, - Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: template, - Parameters: parameters, - ProviderConfig: providerConfig, - }, - } + log.Info("Running bicep build on " + path.Join(filepath, filename)) - if err := r.Create(ctx, deploymentTemplate); err != nil { - log.Error(err, "unable to create deployment template") - } + cmd := exec.Command("/work-dir/bicep", "build", path.Join(filepath, filename), "--stdout") + cmd.Dir = filepath - log.Info("Created Deployment Template", "name", deploymentTemplate.Name) + stdout, err := cmd.CombinedOutput() + if err != nil { + log.Error(err, "failed to run bicep build", "out", string(stdout)) + return "", err } + + log.Info("Bicep build output", "output", string(stdout)) + + return string(stdout), nil } -func (r *GitRepositoryWatcher) processBicepFile(ctx context.Context, path string) (string, string, string) { +func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath, filename string) (armJSON string, err error) { log := ctrl.LoggerFrom(ctx) - _, err := os.ReadFile(path) + log.Info("Running bicep build-params on " + filename) + + cmd := exec.Command("/work-dir/bicep", "build-params", path.Join(filepath, filename), "--stdout") + + stdout, err := cmd.Output() if err != nil { - log.Error(err, "unable to read file") - return "", "", "" + log.Error(err, "failed to run bicep build") + return "", err + } + + log.Info("Bicep build output", "output", string(stdout)) + + return string(stdout), nil +} + +func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Context, fileName, template, parameters, providerConfig string) { + log := ctrl.LoggerFrom(ctx) + + deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: fileName, + Namespace: RadiusSystemNamespace, + }, + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + }, } - // TODOWILLSMITH: compilebicep + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(deploymentTemplate), deploymentTemplate); err != nil { + if client.IgnoreNotFound(err) != nil { + log.Error(err, "unable to get deployment template") + return + } + + if err := r.Client.Create(ctx, deploymentTemplate); err != nil { + log.Error(err, "unable to create deployment template") + } + + log.Info("Created Deployment Template", "name", deploymentTemplate.Name) + return + } + + if err := r.Client.Update(ctx, deploymentTemplate); err != nil { + log.Error(err, "unable to create deployment template") + } - return "", "", "" + log.Info("Updated Deployment Template", "name", deploymentTemplate.Name) } diff --git a/pkg/controller/reconciler/gitrepository_watcher_test.go b/pkg/controller/reconciler/gitrepository_watcher_test.go new file mode 100644 index 0000000000..ebbfa22f2f --- /dev/null +++ b/pkg/controller/reconciler/gitrepository_watcher_test.go @@ -0,0 +1,26 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import "testing" + +func Test_GitRepositoryWatcher_Basic(t *testing.T) { + //TODOWILLSMITH: finish this test + t.Errorf("Test not implemented") +} + +// TODOWILLSMITH: Add more tests diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index fee8540702..4409e41e1f 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -17,6 +17,7 @@ limitations under the License. package reconciler import ( + "encoding/json" "fmt" "testing" "time" @@ -191,3 +192,32 @@ func makeDeployment(name types.NamespacedName) *appsv1.Deployment { func boolPtr(b bool) *bool { return &b } + +func makeDeploymentTemplate(name types.NamespacedName, template map[string]any) *radappiov1alpha3.DeploymentTemplate { + b, err := json.Marshal(template) + if err != nil { + panic(err) + } + + return &radappiov1alpha3.DeploymentTemplate{ + ObjectMeta: ctrl.ObjectMeta{ + Namespace: name.Namespace, + Name: name.Name, + }, + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: string(b), + }, + } +} + +func makeDeploymentResource(name types.NamespacedName, id string) *radappiov1alpha3.DeploymentResource { + return &radappiov1alpha3.DeploymentResource{ + ObjectMeta: ctrl.ObjectMeta{ + Namespace: name.Namespace, + Name: name.Name, + }, + Spec: radappiov1alpha3.DeploymentResourceSpec{ + ID: id, + }, + } +} diff --git a/pkg/controller/service.go b/pkg/controller/service.go index 2aaf3c4a0e..d672a335f6 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -33,6 +33,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var ( @@ -42,6 +44,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(radappiov1alpha3.AddToScheme(scheme)) + utilruntime.Must(sourcev1.AddToScheme(scheme)) } var _ hosting.Service = (*Service)(nil) @@ -125,6 +128,13 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentResource", err) } + err = (&reconciler.GitRepositoryWatcher{ + Client: mgr.GetClient(), + HttpRetry: reconciler.GitRepositoryHttpRetryCount, + }).SetupWithManager(mgr) + if err != nil { + return fmt.Errorf("failed to setup %s controller: %w", "GitRepositoryWatcher", err) + } if s.TLSCertDir == "" { logger.Info("Webhooks will be skipped. TLS certificates not present.") From 2b3e3ba4c3bd91545a98cb43700e5f9af19d4cdb Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 24 Oct 2024 09:16:35 -0700 Subject: [PATCH 06/65] it works Signed-off-by: willdavsmith --- build/build.mk | 8 ++++ build/docker.mk | 3 +- .../radius/radapp.io_deploymentresources.yaml | 8 +++- .../radius/radapp.io_deploymenttemplates.yaml | 10 +--- .../crds/ucpd/ucp.dev_queuemessages.yaml | 2 +- deploy/Chart/crds/ucpd/ucp.dev_resources.yaml | 2 +- .../templates/controller/deployment.yaml | 25 ++++------ deploy/Chart/values.yaml | 5 ++ deploy/images/bicep/Dockerfile | 12 +++++ deploy/images/controller/Dockerfile | 13 +++-- .../v1alpha3/deploymentresource_types.go | 3 +- .../v1alpha3/deploymenttemplate_types.go | 3 +- .../deploymenttemplate_reconciler.go | 18 +++++-- .../deploymenttemplate_reconciler_test.go | 21 ++++++--- .../reconciler/gitrepository_watcher.go | 47 +++++++++++++++---- 15 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 deploy/images/bicep/Dockerfile diff --git a/build/build.mk b/build/build.mk index f173721c5b..2ecc06a3c3 100644 --- a/build/build.mk +++ b/build/build.mk @@ -154,3 +154,11 @@ clean: ## Cleans output directory. lint: ## Runs golangci-lint $(GOLANGCI_LINT) run --fix --timeout 5m +define generateBicepBuildTarget +.PHONY: build-bicep-$(1)-$(2) +build-bicep-$(1)-$(2): + @echo "$(ARROW) Building bicep on $(1)/$(2)" +endef + +# Generate bicep build targets for each combination of OS and ARCH +$(foreach ARCH,$(GOARCHES),$(foreach OS,$(GOOSES),$(eval $(call generateBicepBuildTarget,$(OS),$(ARCH))))) diff --git a/build/docker.mk b/build/docker.mk index 7adcdcaa6b..24fa602585 100644 --- a/build/docker.mk +++ b/build/docker.mk @@ -104,7 +104,8 @@ APPS_MAP := ucpd:./deploy/images/ucpd \ dynamic-rp:./deploy/images/dynamic-rp \ controller:./deploy/images/controller \ testrp:./test/testrp \ - magpiego:./test/magpiego + magpiego:./test/magpiego \ + bicep:./deploy/images/bicep # Function to extract the name and the directory of the Dockerfile from the app string define parseApp diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 5221f6e711..811fcd9fb0 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -43,12 +43,16 @@ spec: id: description: ID is the resource ID. type: string - required: - - id + providerConfig: + description: ProviderConfig specifies the scope for resources + type: string type: object status: description: DeploymentResourceStatus defines the observed state of DeploymentResource properties: + id: + description: ID is the resource ID. + type: string message: description: Message is a human-readable description of the status of the Deployment Resource. diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index 6897b37e15..c947ce2fd9 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -50,9 +50,6 @@ spec: description: Template is the ARM JSON manifest that defines the resources to deploy. type: string - required: - - parameters - - template type: object status: description: DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -80,8 +77,8 @@ spec: type: string type: object outputResources: - description: OutputResources is a list of the resourceIds that were - created by the template. + description: OutputResources is a list of the resourceIDs that were + created by the template on the last deployment. items: type: string type: array @@ -102,9 +99,6 @@ spec: description: Template is the ARM JSON manifest that defines the resources to deploy. type: string - required: - - parameters - - template type: object type: object served: true diff --git a/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml b/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml index 03dda2fcd1..925ce30e1f 100644 --- a/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml +++ b/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: queuemessages.ucp.dev spec: group: ucp.dev diff --git a/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml b/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml index d4ab40029d..bede700f8e 100644 --- a/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml +++ b/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.0 + controller-gen.kubebuilder.io/version: v0.16.4 name: resources.ucp.dev spec: group: ucp.dev diff --git a/deploy/Chart/templates/controller/deployment.yaml b/deploy/Chart/templates/controller/deployment.yaml index f386850cf5..c4ba47a6ac 100644 --- a/deploy/Chart/templates/controller/deployment.yaml +++ b/deploy/Chart/templates/controller/deployment.yaml @@ -27,6 +27,13 @@ spec: {{- end }} spec: serviceAccountName: controller + initContainers: + - name: bicep + image: "{{ .Values.bicep.image }}:{{ .Values.bicep.tag | default $appversion }}" + command: ['sh', '-c', 'mv ./bicep /usr/local/bin/bicep'] + volumeMounts: + - name: bicep + mountPath: /usr/local/bin containers: - name: controller image: "{{ .Values.controller.image }}:{{ .Values.controller.tag | default $appversion }}" @@ -62,6 +69,8 @@ spec: resources:{{ toYaml .Values.controller.resources | nindent 10 }} {{- end }} volumeMounts: + - name: bicep + mountPath: /usr/local/bin - name: config-volume mountPath: /etc/config - name: cert @@ -72,22 +81,8 @@ spec: mountPath: {{ .Values.global.rootCA.mountPath }} readOnly: true {{- end }} - - name: work-dir - mountPath: /work-dir - initContainers: - - name: bicep - image: "ghcr.io/willdavsmith/bicep:latest" - imagePullPolicy: 'Always' - # restartPolicy: Always - command: - - cp - - "/bicep" - - "/work-dir/bicep" - volumeMounts: - - name: work-dir - mountPath: "/work-dir" volumes: - - name: work-dir + - name: bicep emptyDir: {} - name: config-volume configMap: diff --git a/deploy/Chart/values.yaml b/deploy/Chart/values.yaml index d750295570..be39fdaf7d 100644 --- a/deploy/Chart/values.yaml +++ b/deploy/Chart/values.yaml @@ -112,3 +112,8 @@ dashboard: memory: "60Mi" limits: memory: "300Mi" + +bicep: + image: ghcr.io/radius-project/bicep + # Default tag uses Chart AppVersion. + # tag: latest diff --git a/deploy/images/bicep/Dockerfile b/deploy/images/bicep/Dockerfile new file mode 100644 index 0000000000..cb0657a98e --- /dev/null +++ b/deploy/images/bicep/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:latest + +ARG TARGETARCH + +RUN apk --no-cache add curl + +WORKDIR / + +RUN curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 \ + && chmod +x ./bicep + +ENTRYPOINT ["/bin/sh"] diff --git a/deploy/images/controller/Dockerfile b/deploy/images/controller/Dockerfile index f459dfe358..72b1ed8284 100644 --- a/deploy/images/controller/Dockerfile +++ b/deploy/images/controller/Dockerfile @@ -1,5 +1,4 @@ -# Use distroless image which already includes ca-certificates -FROM ubuntu +FROM ubuntu:latest # Argument for target architecture ARG TARGETARCH @@ -7,11 +6,15 @@ ARG TARGETARCH # Set the working directory WORKDIR / +# Install the required dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libicu-dev \ + ca-certificates && \ + rm -rf /var/lib/apt/lists/* + # Copy the application binary for the specified architecture COPY ./linux_${TARGETARCH:-amd64}/release/controller / -# Set the user to non-root (65532:65532 is the default non-root user in distroless) -# USER 65532:65532 - # Set the entrypoint to the application binary ENTRYPOINT ["/controller"] diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 6cd2c72969..e5698eec8a 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha3 import ( - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,7 +35,7 @@ type DeploymentResourceStatus struct { ID string `json:"id,omitempty"` // ProviderConfig specifies the scope for resources - ProviderConfig sdkclients.ProviderConfig `json:"providerConfig,omitempty"` + ProviderConfig string `json:"providerConfig,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index ced2a182c1..28b3987874 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -17,7 +17,6 @@ limitations under the License. package v1alpha3 import ( - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -45,7 +44,7 @@ type DeploymentTemplateStatus struct { Parameters string `json:"parameters,omitempty"` // ProviderConfig specifies the scope for resources - ProviderConfig sdkclients.ProviderConfig `json:"providerConfig,omitempty"` + ProviderConfig string `json:"providerConfig,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 0f0cea75b1..0d275bb9e8 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -110,7 +110,8 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - poller, err := r.Radius.Resources(deploymentTemplate.Status.ProviderConfig.Deployments.Value.Scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + scope, err := parseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + poller, err := r.Radius.Resources(scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -227,7 +228,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name - deploymentTemplate.Status.ProviderConfig = providerConfig + deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { @@ -389,7 +390,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - deploymentTemplate.Status.ProviderConfig = providerConfig + deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -508,6 +509,17 @@ func (r *DeploymentTemplateReconciler) requeueDelay() time.Duration { return delay } +func parseDeploymentScopeFromProviderConfig(providerConfig string) (string, error) { + config := sdkclients.ProviderConfig{} + json.Unmarshal([]byte(providerConfig), &config) + + if config.Deployments == nil { + return "", fmt.Errorf("providerConfig.Deployments is nil") + } + + return config.Deployments.Value.Scope, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *DeploymentTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index ab350b075e..2d86e20035 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -105,7 +105,10 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { // Deployment will be waiting for template to complete provisioning. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", status.ProviderConfig.Deployments.Value.Scope) + + scope, err := parseDeploymentScopeFromProviderConfig(status.ProviderConfig) + require.NoError(t, err) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) radius.CompleteOperation(status.Operation.ResumeToken, nil) @@ -113,7 +116,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { status = waitForDeploymentTemplateStateReady(t, client, name) require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic/providers/Microsoft.Resources/deployments/test-DeploymentTemplate", status.Resource) - resource, err := radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) require.NoError(t, err) expectedProperties := map[string]any{ @@ -164,7 +167,9 @@ func Test_DeploymentTemplateReconciler_ChangeEnvironmentAndApplication(t *testin // Deployment will be waiting for template to complete provisioning. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-change", status.ProviderConfig.Deployments.Value.Scope) + scope, err := parseDeploymentScopeFromProviderConfig(status.ProviderConfig) + require.NoError(t, err) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) radius.CompleteOperation(status.Operation.ResumeToken, nil) @@ -172,7 +177,7 @@ func Test_DeploymentTemplateReconciler_ChangeEnvironmentAndApplication(t *testin status = waitForDeploymentTemplateStateReady(t, client, name) require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-change/providers/Microsoft.Resources/deployments/test-DeploymentTemplate-change", status.Resource) - _, err = radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + _, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) require.NoError(t, err) createEnvironment(radius, "new-environment") @@ -188,15 +193,19 @@ func Test_DeploymentTemplateReconciler_ChangeEnvironmentAndApplication(t *testin // Deletion of the deployment is in progress. status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + scope, err = parseDeploymentScopeFromProviderConfig(status.ProviderConfig) + require.NoError(t, err) + require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) + radius.CompleteOperation(status.Operation.ResumeToken, nil) // Resource should be gone. - _, err = radius.Resources(status.ProviderConfig.Deployments.Value.Scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + _, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) require.Error(t, err) // Deployment will be waiting for extender to complete provisioning. status = waitForDeploymentTemplateStateUpdating(t, client, name, nil) - require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application", status.ProviderConfig.Deployments.Value.Scope) + require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application", scope) radius.CompleteOperation(status.Operation.ResumeToken, nil) // Deployment will update after operation completes diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go index e362f6e524..0946aedcc6 100644 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -2,12 +2,14 @@ package reconciler import ( "context" + "encoding/json" "fmt" "os" "os/exec" "path" "strings" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -94,10 +96,35 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) // for now, we assume the parameters file is the same name as the bicep file // in the same directory // e.g. main.bicep -> main.bicepparam + parameters := "{}" parametersFile := strings.ReplaceAll(f.Name(), ".bicep", ".bicepparam") - parameters, err := r.runBicepBuildParams(ctx, tmpDir, parametersFile) - providerConfig := "providerConfig" + // if it exists + if _, err := os.Stat(path.Join(tmpDir, parametersFile)); err == nil { + parameters, err = r.runBicepBuildParams(ctx, tmpDir, parametersFile) + if err != nil { + log.Error(err, "failed to run bicep build-params") + return ctrl.Result{}, err + } + } + + // TODOWILLSMITH: ??? + var config sdkclients.ProviderConfig + + config.Radius = &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/" + "default", + }, + } + config.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/" + "default", + }, + } + + providerConfig, err := json.Marshal(config) if err != nil { log.Error(err, "failed to run bicep build-params") return ctrl.Result{}, err @@ -112,7 +139,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) // think ab multiple git repos scenario // need to save name of git repo in deployment template? - r.createOrUpdateDeploymentTemplate(ctx, f.Name(), template, parameters, providerConfig) + r.createOrUpdateDeploymentTemplate(ctx, f.Name(), template, parameters, string(providerConfig)) } } @@ -120,21 +147,25 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) } func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, filename string) (armJSON string, err error) { - // TODOWILLSMITH: bicep build is broken log := ctrl.LoggerFrom(ctx) log.Info("Running bicep build on " + path.Join(filepath, filename)) - cmd := exec.Command("/work-dir/bicep", "build", path.Join(filepath, filename), "--stdout") + cmd := exec.Command("bicep", "build", path.Join(filepath, filename), "--outfile", path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json"))) cmd.Dir = filepath stdout, err := cmd.CombinedOutput() if err != nil { - log.Error(err, "failed to run bicep build", "out", string(stdout)) + log.Error(err, "failed to run bicep build") return "", err } - log.Info("Bicep build output", "output", string(stdout)) + // read the output file + stdout, err = os.ReadFile(path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json"))) + if err != nil { + log.Error(err, "failed to read bicep build output") + return "", err + } return string(stdout), nil } @@ -144,7 +175,7 @@ func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath log.Info("Running bicep build-params on " + filename) - cmd := exec.Command("/work-dir/bicep", "build-params", path.Join(filepath, filename), "--stdout") + cmd := exec.Command("bicep", "build-params", path.Join(filepath, filename), "--stdout") stdout, err := cmd.Output() if err != nil { From 5a60fb99bbb37acf5c1ab198000959dd397d1e91 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 24 Oct 2024 20:35:00 -0700 Subject: [PATCH 07/65] fixing bugs Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 10 +- .../radius/radapp.io_deploymenttemplates.yaml | 22 +- .../v1alpha3/deploymentresource_types.go | 2 + .../v1alpha3/deploymenttemplate_types.go | 9 + .../deploymentresource_reconciler.go | 45 +++- .../deploymentresource_reconciler_test.go | 2 - .../deploymenttemplate_reconciler.go | 216 ++++++++++-------- .../deploymenttemplate_reconciler_test.go | 2 - .../gitrepository_predicate_test.go | 189 ++++++++++++++- .../reconciler/gitrepository_watcher.go | 174 +++++++++----- .../reconciler/gitrepository_watcher_test.go | 4 +- 11 files changed, 505 insertions(+), 170 deletions(-) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 811fcd9fb0..0e2b9c6b4f 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -8,13 +8,21 @@ metadata: spec: group: radapp.io names: + categories: + - all + - radius kind: DeploymentResource listKind: DeploymentResourceList plural: deploymentresources singular: deploymentresource scope: Namespaced versions: - - name: v1alpha3 + - additionalPrinterColumns: + - description: Status of the resource + jsonPath: .status.phrase + name: Status + type: string + name: v1alpha3 schema: openAPIV3Schema: description: DeploymentResource is the Schema for the DeploymentResources diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index c947ce2fd9..dcd3d65c04 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -8,13 +8,25 @@ metadata: spec: group: radapp.io names: + categories: + - all + - radius kind: DeploymentTemplate listKind: DeploymentTemplateList plural: deploymenttemplates singular: deploymenttemplate scope: Namespaced versions: - - name: v1alpha3 + - additionalPrinterColumns: + - description: Status of the resource + jsonPath: .status.phrase + name: Status + type: string + - description: Repository of the resource + jsonPath: .status.repository + name: Repository + type: string + name: v1alpha3 schema: openAPIV3Schema: description: DeploymentTemplate is the Schema for the deploymenttemplates @@ -46,6 +58,10 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string + repository: + description: Repository is the name of the GitRepository that contains + the Bicep file. + type: string template: description: Template is the ARM JSON manifest that defines the resources to deploy. @@ -92,6 +108,10 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string + repository: + description: Repository is the name of the GitRepository that contains + the Bicep file. + type: string resource: description: Resource is the resource id of the deployment. type: string diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index e5698eec8a..0d574a52b3 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -69,6 +69,8 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:categories={"all","radius"} +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" // DeploymentResource is the Schema for the DeploymentResources API type DeploymentResource struct { diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 28b3987874..ecbc7355ae 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -30,6 +30,9 @@ type DeploymentTemplateSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` + + // Repository is the name of the GitRepository that contains the Bicep file. + Repository string `json:"repository,omitempty"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -46,6 +49,9 @@ type DeploymentTemplateStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` + // Repository is the name of the GitRepository that contains the Bicep file. + Repository string `json:"repository,omitempty"` + // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` @@ -84,6 +90,9 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:resource:categories={"all","radius"} +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" +// +kubebuilder:printcolumn:name="Repository",type="string",JSONPath=".status.repository",description="Repository of the resource" // DeploymentTemplate is the Schema for the deploymenttemplates API type DeploymentTemplate struct { diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 25b9cbf418..3995b1183e 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -74,10 +74,23 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R // Our algorithm is as follows: // - // TODOWILLSMITH: put algorithm here + // 1. Check if there is an in-progress deletion. If so, check its status: + // 1. If the deletion is still in progress, then queue another reconcile operation and continue processing. + // 2. If the deletion completed successfully, then remove the `radapp.io/deployment-resource-finalizer` finalizer from the resource and continue processing. + // 3. If the operation failed, then update the `status.phrase` and `status.message` as `Failed`. + // 2. If the `DeploymentTemplate` is being deleted, then process deletion: + // 1. Send a DELETE operation to the Radius API to delete the resource specified in the `spec.resourceId` field. + // 2. Continue processing. + // 3. If the `DeploymentTemplate` is not being deleted then process this as a create or update: + // 1. Set the `status.phrase` for the `DeploymentResource` to `Ready`. + // 2. Continue processing. // // We do it this way because it guarantees that we only have one operation going at a time. + if DeploymentResource.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &DeploymentResource) + } + if DeploymentResource.Status.Operation != nil { result, err := r.reconcileOperation(ctx, &DeploymentResource) if err != nil { @@ -93,10 +106,6 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R } } - if DeploymentResource.DeletionTimestamp != nil { - return r.reconcileDelete(ctx, &DeploymentResource) - } - // Nothing to do here, continue processing return ctrl.Result{}, nil } @@ -106,7 +115,20 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - providerConfig := sdkclients.ProviderConfig{} + providerConfig := sdkclients.ProviderConfig{ + Radius: &sdkclients.Radius{ + Type: "radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + }, + Deployments: &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + }, + } err := json.Unmarshal([]byte(deploymentResource.Spec.ProviderConfig), &providerConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) @@ -136,7 +158,6 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d deploymentResource.Status.Operation = nil deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseFailed deploymentResource.Status.Message = err.Error() - err = r.Client.Status().Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err @@ -149,6 +170,11 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d // // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentResource.Status.Operation = nil + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } @@ -158,7 +184,6 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d deploymentResource.Status.Operation = nil deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseFailed - err := r.Client.Status().Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err @@ -175,6 +200,10 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + err := r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } poller, err := r.startDeleteOperation(ctx, deploymentResource) if err != nil { diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index 062a080d6f..a2aaf63399 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -175,5 +175,3 @@ func waitForDeploymentResourceDeleted(t *testing.T, client client.Client, name t }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "DeploymentResource still exists") } - -// TODOWILLSMITH: add more tests diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 0d275bb9e8..e2207c95b4 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -79,10 +79,29 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R // Our algorithm is as follows: // - // TODOWILLSMITH: put algorithm here + // 1. Check if there is an in-progress operation. If so, check its status: + // 1. If the operation is still in progress, then queue another reconcile operation and continue processing. + // 2. If the operation completed successfully: + // 1. Diff the resources in the `properties.outputResources` field returned by the Radius API with the resources in the `status.outputResources` field on the `DeploymentTemplate` resource. + // 2. Depending on the diff, create or delete `DeploymentResource` resources on the cluster. In the case of create, add the `DeploymentTemplate` as the owner of the `DeploymentResource` and set the `radapp.io/deployment-resource-finalizer` finalizer on the `DeploymentResource`. + // 3. Update the `status.phrase` for the `DeploymentTemplate` to `Ready`. + // 4. Continue processing. + // 3. If the operation failed, then update the `status.phrase` and `status.message` as `Failed` with the reason for the failure and continue processing. + // 2. If the `DeploymentTemplate` is being deleted, then process deletion: + // 1. Remove the `radapp.io/deployment-template-finalizer` finalizer from the `DeploymentTemplate`. + // 1. Since the `DeploymentResources` are owned by the `DeploymentTemplate`, the `DeploymentResource` resources will be deleted first. Once they are deleted, the `DeploymentTemplate` resource will be deleted. + // 4. If the `DeploymentTemplate` is not being deleted then process this as a create or update: + // 1. Add the `radapp.io/deployment-template-finalizer` finalizer onto the `DeploymentTemplate` resource. + // 2. Queue a PUT operation against the Radius API to deploy the ARM JSON in the `spec.template` field with the parameters in the `spec.parameters` field. + // 3. Set the `status.phrase` for the `DeploymentTemplate` to `Updating` and the `status.operation` to the operation returned by the Radius API. + // 4. Continue processing. // // We do it this way because it guarantees that we only have one operation going at a time. + if deploymentTemplate.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &deploymentTemplate) + } + if deploymentTemplate.Status.Operation != nil { result, err := r.reconcileOperation(ctx, &deploymentTemplate) if err != nil { @@ -98,10 +117,6 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R } } - if deploymentTemplate.DeletionTimestamp != nil { - return r.reconcileDelete(ctx, &deploymentTemplate) - } - return r.reconcileUpdate(ctx, &deploymentTemplate) } @@ -111,6 +126,10 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { scope, err := parseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to parse deployment scope: %w", err) + } + poller, err := r.Radius.Resources(scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) @@ -135,7 +154,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed deploymentTemplate.Status.Message = err.Error() - err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -146,15 +164,15 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger.Info("Creating output resources.") - //TODOWILLSMITH: clean this up + // Get outputResources from the response outputResources := make([]string, 0) outputResourceList := resp.Properties["outputResources"].([]any) for _, resource := range outputResourceList { - resource2 := resource.(map[string]any) - outputResources = append(outputResources, resource2["id"].(string)) + outputResource := resource.(map[string]any) + outputResources = append(outputResources, outputResource["id"].(string)) } - // compare outputResources with existing DeploymentResources + // Compare outputResources with existing DeploymentResources // if is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it // if is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it // if is present in both, do nothing @@ -171,7 +189,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d for _, outputResourceId := range outputResources { if _, ok := existingOutputResources[outputResourceId]; !ok { - // resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + // Resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it resourceName := generateDeploymentResourceName(outputResourceId) deploymentResource := &radappiov1alpha3.DeploymentResource{ @@ -180,15 +198,18 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d Namespace: deploymentTemplate.Namespace, }, Spec: radappiov1alpha3.DeploymentResourceSpec{ - ID: outputResourceId, + ID: outputResourceId, + ProviderConfig: deploymentTemplate.Spec.ProviderConfig, }, } if controllerutil.AddFinalizer(deploymentResource, DeploymentResourceFinalizer) { + // Add the DeploymentTemplate as the owner of the DeploymentResource if err := controllerutil.SetControllerReference(deploymentTemplate, deploymentResource, r.Scheme); err != nil { return ctrl.Result{}, err } + // Create the DeploymentResource err = r.Client.Create(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err @@ -199,7 +220,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d for _, resource := range deploymentTemplate.Status.OutputResources { if _, ok := newOutputResources[resource]; !ok { - // resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + // Resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it logger.Info("Deleting resource.", "resourceId", resource) resourceName := generateDeploymentResourceName(resource) err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ @@ -229,11 +250,23 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig - return ctrl.Result{}, nil + deploymentTemplate.Status.Repository = deploymentTemplate.Spec.Repository + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting + err := r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + providerConfig := sdkclients.ProviderConfig{} - err := json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) + err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) } @@ -262,7 +295,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed deploymentTemplate.Status.Message = err.Error() - err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -276,6 +308,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Resource = "" + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } @@ -285,7 +322,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - err := r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -297,6 +333,8 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) + logger.Info("Reconciling resource.") + // Ensure that our finalizer is present before we start any operations. if controllerutil.AddFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { err := r.Client.Update(ctx, deploymentTemplate) @@ -310,8 +348,12 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + err := r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } - updatePoller, deletePoller, err := r.startPutOrDeleteOperationIfNeeded(ctx, deploymentTemplate) + updatePoller, err := r.startPutOperationIfNeeded(ctx, deploymentTemplate) if err != nil { logger.Error(err, "Unable to create or update resource.") r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) @@ -330,21 +372,6 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl return ctrl.Result{}, err } - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } else if deletePoller != nil { - // We've successfully started an operation. Update the status and requeue. - token, err := deletePoller.ResumeToken() - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) - } - - deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} - deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } @@ -364,104 +391,112 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) + logger.Info("Resource is being deleted.") + // Since we're going to reconcile, update the observed generation. // // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - - poller, err := r.startDeleteOperationIfNeeded(ctx, deploymentTemplate) + err := r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { - logger.Error(err, "Unable to delete resource.") - r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) return ctrl.Result{}, err - } else if poller != nil { - // We've successfully started an operation. Update the status and requeue. - token, err := poller.ResumeToken() - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) - } + } - providerConfig := sdkclients.ProviderConfig{} - err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + // List all DeploymentResource objects in the same namespace + deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} + err = r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentTemplate.Namespace)) + if err != nil { + return ctrl.Result{}, nil + } + + // Filter the list to include only those owned by the current DeploymentTemplate + var ownedResources []radappiov1alpha3.DeploymentResource + for _, resource := range deploymentResourceList.Items { + if isOwnedBy(resource, deploymentTemplate) { + ownedResources = append(ownedResources, resource) } + } - deploymentTemplate.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} + // If there are still owned DeploymentResources, we need to trigger deletion and wait for them + // to be deleted before we can delete the DeploymentTemplate. + if len(ownedResources) > 0 { + logger.Info("Owned resources still exist, waiting for deletion.") deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err } + // Trigger deletion of owned resources + for _, resource := range ownedResources { + err := r.Client.Delete(ctx, &resource) + if err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } logger.Info("Resource is deleted.") - // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the - // DeploymentTemplate + // At this point we've cleaned up everything. We can remove the finalizer which will allow + // deletion of the DeploymentTemplate if controllerutil.RemoveFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { - err := r.Client.Update(ctx, deploymentTemplate) + deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleted + err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err } - deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") + return ctrl.Result{}, nil } - deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleted - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } + // If we get here, then we're in a bad state. We should have removed the finalizer, but we didn't. + // We should requeue and try again. - r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") - return ctrl.Result{}, nil + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } -func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) // If the resource is already created and is up-to-date, then we don't need to do anything. - if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && deploymentTemplate.Status.Parameters == deploymentTemplate.Spec.Parameters { + if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && + deploymentTemplate.Status.Parameters == deploymentTemplate.Spec.Parameters && + deploymentTemplate.Status.Repository == deploymentTemplate.Spec.Repository && + deploymentTemplate.Status.ProviderConfig == deploymentTemplate.Spec.ProviderConfig { logger.Info("Resource is already created and is up-to-date.") - return nil, nil, nil + return nil, nil } - logger.Info("Template or parameters have changed, starting PUT operation.") + logger.Info("Template, parameters, repository, or providerConfig have changed, starting PUT operation.") var template any err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) if err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal template: %w", err) + return nil, fmt.Errorf("failed to unmarshal template: %w", err) } var parameters any err = json.Unmarshal([]byte(deploymentTemplate.Spec.Parameters), ¶meters) if err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal parameters: %w", err) + return nil, fmt.Errorf("failed to unmarshal parameters: %w", err) } - // TODO PR: Is there a better way to check for all of this stuff? providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { - return nil, nil, fmt.Errorf("failed to unmarshal template: %w", err) + return nil, fmt.Errorf("failed to unmarshal template: %w", err) } if providerConfig.Deployments == nil { - return nil, nil, fmt.Errorf("providerConfig.Deployments is nil") + return nil, fmt.Errorf("providerConfig.Deployments is nil") } if providerConfig.Deployments.Value.Scope == "" { - return nil, nil, fmt.Errorf("providerConfig.Deployments.Value.Scope is empty") - } - if providerConfig.Radius == nil { - return nil, nil, fmt.Errorf("providerConfig.Radius is nil") - } - if providerConfig.Radius.Value.Scope == "" { - return nil, nil, fmt.Errorf("providerConfig.Radius.Value.Scope is empty") + return nil, fmt.Errorf("providerConfig.Deployments.Value.Scope is empty") } logger.Info("Starting PUT operation.") @@ -475,28 +510,18 @@ func (r *DeploymentTemplateReconciler) startPutOrDeleteOperationIfNeeded(ctx con resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) if err != nil { - return nil, nil, err + return nil, err } else if poller != nil { - return poller, nil, nil + return poller, nil } // Update was synchronous deploymentTemplate.Status.Resource = resourceID - return nil, nil, nil -} - -func (r *DeploymentTemplateReconciler) startDeleteOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientDeleteResponse], error) { - logger := ucplog.FromContextOrDiscard(ctx) - if deploymentTemplate.Status.Resource == "" { - logger.Info("Resource is already deleted (or was never created).") - return nil, nil + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return nil, err } - // TODOWILLSMITH: do we need to do anything here? wait for DeploymentResources to be deleted? - - // Deletion was synchronous - - deploymentTemplate.Status.Resource = "" return nil, nil } @@ -520,6 +545,15 @@ func parseDeploymentScopeFromProviderConfig(providerConfig string) (string, erro return config.Deployments.Value.Scope, nil } +func isOwnedBy(resource radappiov1alpha3.DeploymentResource, owner *radappiov1alpha3.DeploymentTemplate) bool { + for _, ownerRef := range resource.OwnerReferences { + if ownerRef.Kind == "DeploymentTemplate" && ownerRef.Name == owner.Name { + return true + } + } + return false +} + // SetupWithManager sets up the controller with the Manager. func (r *DeploymentTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 2d86e20035..713852f393 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -34,8 +34,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -//TODOWILLSMITH: finish this test - const ( DeploymentTemplateTestWaitDuration = time.Second * 10 DeploymentTemplateTestWaitInterval = time.Second * 1 diff --git a/pkg/controller/reconciler/gitrepository_predicate_test.go b/pkg/controller/reconciler/gitrepository_predicate_test.go index 05fef6bee7..e593785a23 100644 --- a/pkg/controller/reconciler/gitrepository_predicate_test.go +++ b/pkg/controller/reconciler/gitrepository_predicate_test.go @@ -19,25 +19,194 @@ package reconciler import ( "testing" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/event" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestGitRepositoryRevisionChangePredicate_Create(t *testing.T) { - //TODOWILLSMITH: finish this test - p := GitRepositoryRevisionChangePredicate{} - e := event.CreateEvent{} + predicate := GitRepositoryRevisionChangePredicate{} + + tests := []struct { + name string + event event.CreateEvent + expected bool + }{ + { + name: "Source is not a sourcev1.Source", + event: event.CreateEvent{ + Object: &corev1.Pod{}, + }, + expected: false, + }, + { + name: "Source has no artifact", + event: event.CreateEvent{ + Object: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + }, + expected: false, + }, + { + name: "Source has an artifact", + event: event.CreateEvent{ + Object: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Path: "test-path", + }, + }, + }, + }, + expected: true, + }, + } - if p.Create(e) { - t.Errorf("expected false, got true") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := predicate.Create(tt.event) + assert.Equal(t, tt.expected, result) + }) } } func TestGitRepositoryRevisionChangePredicate_Update(t *testing.T) { - //TODOWILLSMITH: finish this test - p := GitRepositoryRevisionChangePredicate{} - e := event.UpdateEvent{} + predicate := GitRepositoryRevisionChangePredicate{} + + tests := []struct { + name string + event event.UpdateEvent + expected bool + }{ + { + name: "Source ObjectOld is nil", + event: event.UpdateEvent{ + ObjectNew: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + }, + expected: false, + }, + { + name: "Source ObjectNew is nil", + event: event.UpdateEvent{ + ObjectOld: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + }, + expected: false, + }, + { + name: "Source ObjectOld is not a sourcev1.Source", + event: event.UpdateEvent{ + ObjectOld: &corev1.Pod{}, + ObjectNew: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + }, + expected: false, + }, + { + name: "Source ObjectNew is not a sourcev1.Source", + event: event.UpdateEvent{ + ObjectOld: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + ObjectNew: &corev1.Pod{}, + }, + expected: false, + }, + { + name: "Sources ObjectOld and ObjectNew have no artifact", + event: event.UpdateEvent{ + ObjectOld: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + ObjectNew: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + }, + }, + expected: true, + }, + { + name: "Source ObjectNew and ObjectOld are the same", + event: event.UpdateEvent{ + ObjectOld: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Path: "test-path", + }, + }, + }, + ObjectNew: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Path: "test-path", + }, + }, + }, + }, + expected: true, + }, + { + name: "Source ObjectNew and ObjectOld are the different", + event: event.UpdateEvent{ + ObjectOld: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Path: "test-path", + }, + }, + }, + ObjectNew: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Path: "test-path-different", + }, + }, + }, + }, + expected: true, + }, + } - if p.Update(e) { - t.Errorf("expected false, got true") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := predicate.Update(tt.event) + assert.Equal(t, tt.expected, result) + }) } } diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go index 0946aedcc6..450d2463f5 100644 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -9,7 +9,10 @@ import ( "path" "strings" + "github.com/go-logr/logr" + "github.com/google/martian/log" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/ucp/ucplog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -22,6 +25,10 @@ import ( radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" ) +const ( + repositoryField = "spec.repository" +) + // GitRepositoryWatcher watches GitRepository objects for revision changes type GitRepositoryWatcher struct { client.Client @@ -30,6 +37,10 @@ type GitRepositoryWatcher struct { } func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentTemplate{}, repositoryField, repositoryIndexer); err != nil { + return err + } + r.artifactFetcher = fetch.New( fetch.WithRetries(r.HttpRetry), fetch.WithMaxDownloadSize(tar.UnlimitedUntarSize), @@ -46,8 +57,10 @@ func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories/status,verbs=get func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := ctrl.LoggerFrom(ctx) + logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) + ctx = logr.NewContext(ctx, logger) + // Get the GitRepository object from the cluster var repository sourcev1.GitRepository if err := r.Get(ctx, req.NamespacedName, &repository); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -56,7 +69,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) artifact := repository.Status.Artifact log.Info("New revision detected", "revision", artifact.Revision) - // create tmp dir + // Create temp dir to store the fetched artifact tmpDir, err := os.MkdirTemp("", repository.Name) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to create temp dir, error: %w", err) @@ -69,126 +82,182 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) } }(tmpDir) + // Fetch the artifact from the Source Controller log.Info("fetching artifact...", "url", artifact.URL) if err := r.artifactFetcher.Fetch(artifact.URL, artifact.Digest, tmpDir); err != nil { log.Error(err, "unable to fetch artifact") return ctrl.Result{}, err } - // list artifact content + log.Info("fetched artifact", "url", artifact.URL) + files, err := os.ReadDir(tmpDir) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to list files, error: %w", err) } - // TODOWILLSMITH: how do we decide which files to run bicep build on? - // for now, we'll just run it on all root files + // TODOWILLSMITH: Where to get ProviderConfig def? + var config sdkclients.ProviderConfig + + config.Radius = &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + } + config.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + } + + providerConfig, err := json.Marshal(config) + if err != nil { + log.Error(err, "failed to run bicep build-params") + return ctrl.Result{}, err + } + + // Run bicep build on all root bicep files for _, f := range files { extension := path.Ext(f.Name()) if extension == ".bicep" { + fileNameBase := strings.TrimSuffix(f.Name(), path.Ext(f.Name())) + deploymentTemplateName := repository.Name + "-" + fileNameBase + template, err := r.runBicepBuild(ctx, tmpDir, f.Name()) if err != nil { log.Error(err, "failed to run bicep build") return ctrl.Result{}, err } - // TODOWILLSMITH: how do we decide which parameters file to use? - // for now, we assume the parameters file is the same name as the bicep file - // in the same directory - // e.g. main.bicep -> main.bicepparam + // Run bicep build-params on the bicepparams that matches the bicep file + // e.g. if the bicep file is main.bicep, the bicepparams file should be main.bicepparam parameters := "{}" - parametersFile := strings.ReplaceAll(f.Name(), ".bicep", ".bicepparam") + parametersFileName := fileNameBase + ".bicepparam" - // if it exists - if _, err := os.Stat(path.Join(tmpDir, parametersFile)); err == nil { - parameters, err = r.runBicepBuildParams(ctx, tmpDir, parametersFile) + // If the bicepparams file exists, run bicep build-params. Otherwise, use the + // default (empty) parameters. + if _, err := os.Stat(path.Join(tmpDir, parametersFileName)); err == nil { + parameters, err = r.runBicepBuildParams(ctx, tmpDir, parametersFileName) if err != nil { log.Error(err, "failed to run bicep build-params") return ctrl.Result{}, err } } - // TODOWILLSMITH: ??? - var config sdkclients.ProviderConfig + // Now we should create (or update) each DeploymentTemplate for the bicep files + // specified in the git repository. - config.Radius = &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/" + "default", - }, - } - config.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/" + "default", - }, - } + // Create or update the deployment template. + log.Info("Creating or updating Deployment Template", "name", deploymentTemplateName) + r.createOrUpdateDeploymentTemplate(ctx, deploymentTemplateName, template, parameters, string(providerConfig), repository.Name) + } + } - providerConfig, err := json.Marshal(config) - if err != nil { - log.Error(err, "failed to run bicep build-params") + // Get all DeploymentTemplates on the cluster that are associated with the git repository. + deploymentTemplates := &radappiov1alpha3.DeploymentTemplateList{} + err = r.Client.List(ctx, deploymentTemplates, client.MatchingFields{repositoryField: repository.Name}) + if err != nil { + log.Error(err, "unable to list deployment templates") + return ctrl.Result{}, err + } + + // For all of the DeploymentTemplates on the cluster, check if the bicep file + // that it was created from still exists in the git repository. If it does not, + // delete the DeploymentTemplate. + for _, deploymentTemplate := range deploymentTemplates.Items { + deploymentTemplateFilename := fmt.Sprintf(strings.TrimPrefix(deploymentTemplate.Name, repository.Name+"-"), ".bicep") + if _, err := os.Stat(path.Join(tmpDir, deploymentTemplateFilename)); err != nil { + // File does not exist in the git repository, + // delete the DeploymentTemplate from the cluster + log.Info("Deleting DeploymentTemplate", "name", deploymentTemplate.Name) + if err := r.Client.Delete(ctx, &deploymentTemplate); err != nil { + log.Error(err, "unable to delete deployment template") return ctrl.Result{}, err } - // TODOWILLSMITH: create/update or delete - // determine if this bicep file has already been deployed, if so update - // if not, create, - // if the bicep file has been deleted, delete the deployment template - - // get all deployment templates on the cluster - // think ab multiple git repos scenario - // need to save name of git repo in deployment template? - - r.createOrUpdateDeploymentTemplate(ctx, f.Name(), template, parameters, string(providerConfig)) + log.Info("Deleted DeploymentTemplate", "name", deploymentTemplate.Name) } } return ctrl.Result{}, nil } +func repositoryIndexer(o client.Object) []string { + deploymentTemplate, ok := o.(*radappiov1alpha3.DeploymentTemplate) + if !ok { + return nil + } + return []string{deploymentTemplate.Spec.Repository} +} + func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, filename string) (armJSON string, err error) { - log := ctrl.LoggerFrom(ctx) + logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) + ctx = logr.NewContext(ctx, logger) log.Info("Running bicep build on " + path.Join(filepath, filename)) - cmd := exec.Command("bicep", "build", path.Join(filepath, filename), "--outfile", path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json"))) + outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json")) + + cmd := exec.Command("bicep", "build", path.Join(filepath, filename), "--outfile", outfile) cmd.Dir = filepath - stdout, err := cmd.CombinedOutput() + // Run the bicep build command + err = cmd.Run() if err != nil { log.Error(err, "failed to run bicep build") return "", err } - // read the output file - stdout, err = os.ReadFile(path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json"))) + // Read the contents of the generated .json file + contents, err := os.ReadFile(outfile) if err != nil { log.Error(err, "failed to read bicep build output") return "", err } - return string(stdout), nil + return string(contents), nil } func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath, filename string) (armJSON string, err error) { - log := ctrl.LoggerFrom(ctx) + logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) + ctx = logr.NewContext(ctx, logger) log.Info("Running bicep build-params on " + filename) - cmd := exec.Command("bicep", "build-params", path.Join(filepath, filename), "--stdout") + outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicepparam", ".bicepparam.json")) + + cmd := exec.Command("bicep", "build-params", path.Join(filepath, filename), "--outfile", outfile) - stdout, err := cmd.Output() + // Run the bicep build-params command + err = cmd.Run() if err != nil { log.Error(err, "failed to run bicep build") return "", err } - log.Info("Bicep build output", "output", string(stdout)) + // Read the contents of the generated .bicepparam.json file + contents, err := os.ReadFile(outfile) + if err != nil { + log.Error(err, "failed to read bicep build-params output") + return "", err + } + + var params map[string]interface{} + err = json.Unmarshal(contents, ¶ms) + + if params["parameters"] == nil { + logger.Info("No parameters found in bicep build-params output") + return "{}", nil + } + + specifiedParams, err := json.Marshal(params["parameters"]) - return string(stdout), nil + return specifiedParams, nil } -func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Context, fileName, template, parameters, providerConfig string) { +func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Context, fileName, template, parameters, providerConfig, repository string) { log := ctrl.LoggerFrom(ctx) deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ @@ -200,6 +269,7 @@ func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Cont Template: template, Parameters: parameters, ProviderConfig: providerConfig, + Repository: repository, }, } diff --git a/pkg/controller/reconciler/gitrepository_watcher_test.go b/pkg/controller/reconciler/gitrepository_watcher_test.go index ebbfa22f2f..f53e712213 100644 --- a/pkg/controller/reconciler/gitrepository_watcher_test.go +++ b/pkg/controller/reconciler/gitrepository_watcher_test.go @@ -20,7 +20,5 @@ import "testing" func Test_GitRepositoryWatcher_Basic(t *testing.T) { //TODOWILLSMITH: finish this test - t.Errorf("Test not implemented") + t.Skip("TODO: finish this test") } - -// TODOWILLSMITH: Add more tests From df3eb33b1d028aa5572bff9986c767769fb16893 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 28 Oct 2024 09:39:54 -0700 Subject: [PATCH 08/65] reconcile app Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 12 +- .../v1alpha3/deploymentresource_types.go | 14 ++- .../deploymentresource_reconciler.go | 115 ++++++++++++++---- .../deploymentresource_reconciler_test.go | 2 +- .../deploymenttemplate_reconciler.go | 106 ++++------------ .../reconciler/gitrepository_watcher.go | 76 +++++++----- pkg/controller/reconciler/shared_test.go | 2 +- .../sample-repo/deploymenttemplate/app.yaml | 0 8 files changed, 181 insertions(+), 146 deletions(-) create mode 100644 test/gitops/sample-repo/deploymenttemplate/app.yaml diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 0e2b9c6b4f..34346afde4 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -49,17 +49,21 @@ spec: description: DeploymentResourceSpec defines the desired state of DeploymentResource properties: id: - description: ID is the resource ID. + description: Id is the resource Id. type: string providerConfig: description: ProviderConfig specifies the scope for resources type: string + repository: + description: Repository is the name of the GitRepository that contains + the Bicep file. + type: string type: object status: description: DeploymentResourceStatus defines the observed state of DeploymentResource properties: id: - description: ID is the resource ID. + description: Id is the resource Id. type: string message: description: Message is a human-readable description of the status @@ -90,6 +94,10 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string + repository: + description: Repository is the name of the GitRepository that contains + the Bicep file. + type: string type: object type: object served: true diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 0d574a52b3..1ae4b62b1c 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -22,21 +22,27 @@ import ( // DeploymentResourceSpec defines the desired state of DeploymentResource type DeploymentResourceSpec struct { - // ID is the resource ID. - ID string `json:"id,omitempty"` + // Id is the resource Id. + Id string `json:"id,omitempty"` // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` + + // Repository is the name of the GitRepository that contains the Bicep file. + Repository string `json:"repository,omitempty"` } // DeploymentResourceStatus defines the observed state of DeploymentResource type DeploymentResourceStatus struct { - // ID is the resource ID. - ID string `json:"id,omitempty"` + // Id is the resource Id. + Id string `json:"id,omitempty"` // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` + // Repository is the name of the GitRepository that contains the Bicep file. + Repository string `json:"repository,omitempty"` + // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 3995b1183e..9783d8deec 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -60,8 +61,8 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "DeploymentResource", "name", req.Name, "namespace", req.Namespace) ctx = logr.NewContext(ctx, logger) - DeploymentResource := radappiov1alpha3.DeploymentResource{} - err := r.Client.Get(ctx, req.NamespacedName, &DeploymentResource) + deploymentResource := radappiov1alpha3.DeploymentResource{} + err := r.Client.Get(ctx, req.NamespacedName, &deploymentResource) if apierrors.IsNotFound(err) { // This can happen due to a data-race if the Deployment Resource is created and then deleted before we can // reconcile it. There's nothing to do here. @@ -87,12 +88,8 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R // // We do it this way because it guarantees that we only have one operation going at a time. - if DeploymentResource.DeletionTimestamp != nil { - return r.reconcileDelete(ctx, &DeploymentResource) - } - - if DeploymentResource.Status.Operation != nil { - result, err := r.reconcileOperation(ctx, &DeploymentResource) + if deploymentResource.Status.Operation != nil { + result, err := r.reconcileOperation(ctx, &deploymentResource) if err != nil { logger.Error(err, "Unable to reconcile in-progress operation.") return ctrl.Result{}, err @@ -106,7 +103,20 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R } } - // Nothing to do here, continue processing + if deploymentResource.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &deploymentResource) + } + + // If we get here then it means we can process the result of the operation. + logger.Info("Resource is in desired state.", "resourceId", deploymentResource.Spec.Id) + + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseReady + err = r.Client.Status().Update(ctx, &deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + r.EventRecorder.Event(&deploymentResource, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") return ctrl.Result{}, nil } @@ -157,7 +167,6 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d deploymentResource.Status.Operation = nil deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseFailed - deploymentResource.Status.Message = err.Error() err = r.Client.Status().Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err @@ -170,10 +179,6 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d // // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentResource.Status.Operation = nil - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{}, nil } @@ -195,14 +200,63 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) + logger.Info("Resource is being deleted.", "resourceId", deploymentResource.Spec.Id) + // Since we're going to reconcile, update the observed generation. // // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentResource.Status.ObservedGeneration = deploymentResource.Generation - err := r.Client.Status().Update(ctx, deploymentResource) + + // Check other resources that depend on this resource. + + // List all DeploymentResource objects in the same namespace + deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} + err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, nil + } + + appsCount := 0 + envsCount := 0 + otherCount := 0 + for _, dr := range deploymentResourceList.Items { + if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { + continue + } + if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { + appsCount++ + } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { + envsCount++ + } else if dr.Spec.Id != "" { + logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) + otherCount++ + } + } + + if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { + // dont delete app until otherCount is 0 + if otherCount > 0 { + logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + } + + if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { + if otherCount > 0 { + logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } } poller, err := r.startDeleteOperation(ctx, deploymentResource) @@ -232,28 +286,31 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the // DeploymentResource if controllerutil.RemoveFinalizer(deploymentResource, DeploymentResourceFinalizer) { - err := r.Client.Update(ctx, deploymentResource) + deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + err = r.Client.Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err } - - deploymentResource.Status.ObservedGeneration = deploymentResource.Generation } - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + logger.Info("Finalizer was not removed, requeueing.") + err = r.Client.Status().Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err } - r.EventRecorder.Event(deploymentResource, corev1.EventTypeNormal, "Reconciled", "Successfully reconciled resource.") - return ctrl.Result{}, nil + // If we get here, then we're in a bad state. We should have removed the finalizer, but we didn't. + // We should requeue and try again. + + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[generated.GenericResourcesClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) - resourceId := deploymentResource.Spec.ID + resourceId := deploymentResource.Spec.Id logger.Info("Starting DELETE operation.") poller, err := deleteResource(ctx, r.Radius, resourceId) @@ -276,8 +333,20 @@ func (r *DeploymentResourceReconciler) requeueDelay() time.Duration { return delay } +func deploymentResourceRepositoryIndexer(o client.Object) []string { + deploymentResource, ok := o.(*radappiov1alpha3.DeploymentResource) + if !ok { + return nil + } + return []string{deploymentResource.Spec.Repository} +} + // SetupWithManager sets up the controller with the Manager. func (r *DeploymentResourceReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentResource{}, repositoryField, deploymentResourceRepositoryIndexer); err != nil { + return err + } + return ctrl.NewControllerManagedBy(mgr). For(&radappiov1alpha3.DeploymentResource{}). Complete(r) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index a2aaf63399..2868f5ff29 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -98,7 +98,7 @@ func Test_DeploymentResourceReconciler_Basic(t *testing.T) { // Deployment will update after operation completes status := waitForDeploymentResourceStateReady(t, client, name) - require.Equal(t, TestDeploymentResourceID, status.ID) + require.Equal(t, TestDeploymentResourceID, status.Id) err = client.Delete(ctx, deployment) require.NoError(t, err) diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index e2207c95b4..f0c5e44e35 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -98,10 +98,6 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R // // We do it this way because it guarantees that we only have one operation going at a time. - if deploymentTemplate.DeletionTimestamp != nil { - return r.reconcileDelete(ctx, &deploymentTemplate) - } - if deploymentTemplate.Status.Operation != nil { result, err := r.reconcileOperation(ctx, &deploymentTemplate) if err != nil { @@ -117,6 +113,10 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R } } + if deploymentTemplate.DeletionTimestamp != nil { + return r.reconcileDelete(ctx, &deploymentTemplate) + } + return r.reconcileUpdate(ctx, &deploymentTemplate) } @@ -153,7 +153,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - deploymentTemplate.Status.Message = err.Error() err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -198,8 +197,9 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d Namespace: deploymentTemplate.Namespace, }, Spec: radappiov1alpha3.DeploymentResourceSpec{ - ID: outputResourceId, + Id: outputResourceId, ProviderConfig: deploymentTemplate.Spec.ProviderConfig, + Repository: deploymentTemplate.Spec.Repository, }, } @@ -251,67 +251,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig deploymentTemplate.Status.Repository = deploymentTemplate.Spec.Repository - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil - } else if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - deploymentTemplate.Status.Operation = nil - deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - err := r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } - - providerConfig := sdkclients.ProviderConfig{} - err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) - } - - poller, err := r.Radius.Resources(providerConfig.Deployments.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) - } - - _, err = poller.Poll(ctx) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to poll operation status: %w", err) - } - - if !poller.Done() { - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } - - // If we get here, the operation is complete. - _, err = poller.Result(ctx) - if err != nil { - // Operation failed, reset state and retry. - r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) - logger.Error(err, "Delete failed.") - - deploymentTemplate.Status.Operation = nil - deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - deploymentTemplate.Status.Message = err.Error() - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } - - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } - - // If we get here, the operation was a success. Update the status and continue. - // - // NOTE: we don't need to save the status here, because we're going to continue reconciling. - deploymentTemplate.Status.Operation = nil - deploymentTemplate.Status.Resource = "" - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{}, nil } @@ -348,10 +287,6 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - err := r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } updatePoller, err := r.startPutOperationIfNeeded(ctx, deploymentTemplate) if err != nil { @@ -391,21 +326,19 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) - logger.Info("Resource is being deleted.") + logger.Info("Resource is being deleted.", "resourceId", deploymentTemplate.Status.Resource) // Since we're going to reconcile, update the observed generation. // // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - err := r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } + deploymentTemplate.Status.Operation = nil + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting // List all DeploymentResource objects in the same namespace deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err = r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentTemplate.Namespace)) + err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentTemplate.Namespace)) if err != nil { return ctrl.Result{}, nil } @@ -422,11 +355,6 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl // to be deleted before we can delete the DeploymentTemplate. if len(ownedResources) > 0 { logger.Info("Owned resources still exist, waiting for deletion.") - deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } // Trigger deletion of owned resources for _, resource := range ownedResources { @@ -436,6 +364,11 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl } } + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } @@ -446,7 +379,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl if controllerutil.RemoveFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleted - err = r.Client.Status().Update(ctx, deploymentTemplate) + err = r.Client.Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err } @@ -455,6 +388,13 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{}, nil } + logger.Info("Finalizer was not removed, requeueing.") + + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + // If we get here, then we're in a bad state. We should have removed the finalizer, but we didn't. // We should requeue and try again. diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go index 450d2463f5..cd4eaf7bca 100644 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ b/pkg/controller/reconciler/gitrepository_watcher.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/go-logr/logr" - "github.com/google/martian/log" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,7 +25,8 @@ import ( ) const ( - repositoryField = "spec.repository" + repositoryField = "spec.repository" + previousArtifactAnnotation = "previous-artifact" ) // GitRepositoryWatcher watches GitRepository objects for revision changes @@ -66,8 +66,14 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, client.IgnoreNotFound(err) } + // Check if the Artifact field is set artifact := repository.Status.Artifact - log.Info("New revision detected", "revision", artifact.Revision) + if artifact == nil { + logger.Info("No artifact found for GitRepository", "name", repository.Name) + return ctrl.Result{}, nil + } + + logger.Info("New revision detected", "revision", artifact.Revision) // Create temp dir to store the fetched artifact tmpDir, err := os.MkdirTemp("", repository.Name) @@ -78,18 +84,18 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) defer func(path string) { err := os.RemoveAll(path) if err != nil { - log.Error(err, "unable to remove temp dir") + logger.Error(err, "unable to remove temp dir") } }(tmpDir) // Fetch the artifact from the Source Controller - log.Info("fetching artifact...", "url", artifact.URL) + logger.Info("fetching artifact...", "url", artifact.URL) if err := r.artifactFetcher.Fetch(artifact.URL, artifact.Digest, tmpDir); err != nil { - log.Error(err, "unable to fetch artifact") + logger.Error(err, "unable to fetch artifact") return ctrl.Result{}, err } - log.Info("fetched artifact", "url", artifact.URL) + logger.Info("fetched artifact", "url", artifact.URL) files, err := os.ReadDir(tmpDir) if err != nil { @@ -114,7 +120,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) providerConfig, err := json.Marshal(config) if err != nil { - log.Error(err, "failed to run bicep build-params") + logger.Error(err, "failed to run bicep build-params") return ctrl.Result{}, err } @@ -127,7 +133,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) template, err := r.runBicepBuild(ctx, tmpDir, f.Name()) if err != nil { - log.Error(err, "failed to run bicep build") + logger.Error(err, "failed to run bicep build") return ctrl.Result{}, err } @@ -141,7 +147,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) if _, err := os.Stat(path.Join(tmpDir, parametersFileName)); err == nil { parameters, err = r.runBicepBuildParams(ctx, tmpDir, parametersFileName) if err != nil { - log.Error(err, "failed to run bicep build-params") + logger.Error(err, "failed to run bicep build-params") return ctrl.Result{}, err } } @@ -150,7 +156,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) // specified in the git repository. // Create or update the deployment template. - log.Info("Creating or updating Deployment Template", "name", deploymentTemplateName) + logger.Info("Creating or updating Deployment Template", "name", deploymentTemplateName) r.createOrUpdateDeploymentTemplate(ctx, deploymentTemplateName, template, parameters, string(providerConfig), repository.Name) } } @@ -159,7 +165,7 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) deploymentTemplates := &radappiov1alpha3.DeploymentTemplateList{} err = r.Client.List(ctx, deploymentTemplates, client.MatchingFields{repositoryField: repository.Name}) if err != nil { - log.Error(err, "unable to list deployment templates") + logger.Error(err, "unable to list deployment templates") return ctrl.Result{}, err } @@ -171,13 +177,13 @@ func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) if _, err := os.Stat(path.Join(tmpDir, deploymentTemplateFilename)); err != nil { // File does not exist in the git repository, // delete the DeploymentTemplate from the cluster - log.Info("Deleting DeploymentTemplate", "name", deploymentTemplate.Name) + logger.Info("Deleting DeploymentTemplate", "name", deploymentTemplate.Name) if err := r.Client.Delete(ctx, &deploymentTemplate); err != nil { - log.Error(err, "unable to delete deployment template") + logger.Error(err, "unable to delete deployment template") return ctrl.Result{}, err } - log.Info("Deleted DeploymentTemplate", "name", deploymentTemplate.Name) + logger.Info("Deleted DeploymentTemplate", "name", deploymentTemplate.Name) } } @@ -193,10 +199,9 @@ func repositoryIndexer(o client.Object) []string { } func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, filename string) (armJSON string, err error) { - logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) - ctx = logr.NewContext(ctx, logger) + logger := ucplog.FromContextOrDiscard(ctx) - log.Info("Running bicep build on " + path.Join(filepath, filename)) + logger.Info("Running bicep build on " + path.Join(filepath, filename)) outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json")) @@ -206,14 +211,14 @@ func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, file // Run the bicep build command err = cmd.Run() if err != nil { - log.Error(err, "failed to run bicep build") + logger.Error(err, "failed to run bicep build") return "", err } // Read the contents of the generated .json file contents, err := os.ReadFile(outfile) if err != nil { - log.Error(err, "failed to read bicep build output") + logger.Error(err, "failed to read bicep build output") return "", err } @@ -221,10 +226,9 @@ func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, file } func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath, filename string) (armJSON string, err error) { - logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) - ctx = logr.NewContext(ctx, logger) + logger := ucplog.FromContextOrDiscard(ctx) - log.Info("Running bicep build-params on " + filename) + logger.Info("Running bicep build-params on " + filename) outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicepparam", ".bicepparam.json")) @@ -233,19 +237,23 @@ func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath // Run the bicep build-params command err = cmd.Run() if err != nil { - log.Error(err, "failed to run bicep build") + logger.Error(err, "failed to run bicep build") return "", err } // Read the contents of the generated .bicepparam.json file contents, err := os.ReadFile(outfile) if err != nil { - log.Error(err, "failed to read bicep build-params output") + logger.Error(err, "failed to read bicep build-params output") return "", err } var params map[string]interface{} err = json.Unmarshal(contents, ¶ms) + if err != nil { + logger.Error(err, "failed to unmarshal bicep build-params output") + return "", err + } if params["parameters"] == nil { logger.Info("No parameters found in bicep build-params output") @@ -253,12 +261,16 @@ func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath } specifiedParams, err := json.Marshal(params["parameters"]) + if err != nil { + logger.Error(err, "failed to marshal parameters") + return "", err + } - return specifiedParams, nil + return string(specifiedParams), nil } func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Context, fileName, template, parameters, providerConfig, repository string) { - log := ctrl.LoggerFrom(ctx) + logger := ucplog.FromContextOrDiscard(ctx) deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: metav1.ObjectMeta{ @@ -275,21 +287,21 @@ func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Cont if err := r.Client.Get(ctx, client.ObjectKeyFromObject(deploymentTemplate), deploymentTemplate); err != nil { if client.IgnoreNotFound(err) != nil { - log.Error(err, "unable to get deployment template") + logger.Error(err, "unable to get deployment template") return } if err := r.Client.Create(ctx, deploymentTemplate); err != nil { - log.Error(err, "unable to create deployment template") + logger.Error(err, "unable to create deployment template") } - log.Info("Created Deployment Template", "name", deploymentTemplate.Name) + logger.Info("Created Deployment Template", "name", deploymentTemplate.Name) return } if err := r.Client.Update(ctx, deploymentTemplate); err != nil { - log.Error(err, "unable to create deployment template") + logger.Error(err, "unable to create deployment template") } - log.Info("Updated Deployment Template", "name", deploymentTemplate.Name) + logger.Info("Updated Deployment Template", "name", deploymentTemplate.Name) } diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index 4409e41e1f..c5a44ef850 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -217,7 +217,7 @@ func makeDeploymentResource(name types.NamespacedName, id string) *radappiov1alp Name: name.Name, }, Spec: radappiov1alpha3.DeploymentResourceSpec{ - ID: id, + Id: id, }, } } diff --git a/test/gitops/sample-repo/deploymenttemplate/app.yaml b/test/gitops/sample-repo/deploymenttemplate/app.yaml new file mode 100644 index 0000000000..e69de29bb2 From 643d38a43f3411d06c873f2d708e5983d64fb820 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 29 Oct 2024 14:34:05 -0700 Subject: [PATCH 09/65] gkm tests Signed-off-by: willdavsmith --- cmd/rad/cmd/bicep.go | 4 +- cmd/rad/cmd/root.go | 4 + pkg/cli/bicep/deployment_parameters.go | 19 +- pkg/cli/bicep/deployment_parameters_test.go | 32 +- .../generatekubernetesmanifest.go | 405 +++++++++++++++ .../generatekubernetesmanifest_test.go | 460 ++++++++++++++++++ .../testdata/aws.yaml | 9 + .../testdata/azure.yaml | 9 + .../testdata/basic.yaml | 9 + .../testdata/value.yaml | 9 + pkg/cli/cmd/deploy/deploy.go | 3 +- pkg/cli/cmd/deploy/deploy_test.go | 1 - pkg/cli/cmd/recipe/register/register.go | 4 +- .../deploymentresource_reconciler.go | 102 ++-- 14 files changed, 992 insertions(+), 78 deletions(-) create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml diff --git a/cmd/rad/cmd/bicep.go b/cmd/rad/cmd/bicep.go index f673ee770d..ea615cd53d 100644 --- a/cmd/rad/cmd/bicep.go +++ b/cmd/rad/cmd/bicep.go @@ -22,8 +22,8 @@ import ( var bicepCmd = &cobra.Command{ Use: "bicep", - Short: "Manage bicep compiler", - Long: `Manage bicep compiler used by Radius`, + Short: "Handle bicep-specific tasks for Radius", + Long: `Handle bicep-specific tasks for Radius`, } func init() { diff --git a/cmd/rad/cmd/root.go b/cmd/rad/cmd/root.go index 466c26e0b0..4440d54ef0 100644 --- a/cmd/rad/cmd/root.go +++ b/cmd/rad/cmd/root.go @@ -35,6 +35,7 @@ import ( app_list "github.com/radius-project/radius/pkg/cli/cmd/app/list" app_show "github.com/radius-project/radius/pkg/cli/cmd/app/show" app_status "github.com/radius-project/radius/pkg/cli/cmd/app/status" + bicep_generate_kubernetes_manifest "github.com/radius-project/radius/pkg/cli/cmd/bicep/generatekubernetesmanifest" bicep_publish "github.com/radius-project/radius/pkg/cli/cmd/bicep/publish" credential "github.com/radius-project/radius/pkg/cli/cmd/credential" cmd_deploy "github.com/radius-project/radius/pkg/cli/cmd/deploy" @@ -326,6 +327,9 @@ func initSubCommands() { bicepPublishCmd, _ := bicep_publish.NewCommand(framework) bicepCmd.AddCommand(bicepPublishCmd) + bicepGenerateKubernetesManifestCmd, _ := bicep_generate_kubernetes_manifest.NewCommand(framework) + bicepCmd.AddCommand(bicepGenerateKubernetesManifestCmd) + installCmd := install.NewCommand() RootCmd.AddCommand(installCmd) diff --git a/pkg/cli/bicep/deployment_parameters.go b/pkg/cli/bicep/deployment_parameters.go index ceede46273..de37cede94 100644 --- a/pkg/cli/bicep/deployment_parameters.go +++ b/pkg/cli/bicep/deployment_parameters.go @@ -19,32 +19,23 @@ package bicep import ( "encoding/json" "fmt" - "io/fs" - "os" "strings" + "github.com/spf13/afero" + "github.com/radius-project/radius/pkg/cli/clients" ) // ParameterParser is used to parse the parameters as part of the `rad deploy` command. See the docs for `rad deploy` for examples // of what we need to support here. type ParameterParser struct { - FileSystem fs.FS -} - -type OSFileSystem struct { + FileSystem afero.Fs } type ParameterFile struct { Parameters clients.DeploymentParameters `json:"parameters"` } -// The Open function opens the file specified by the name parameter and returns a file object and an error if the file -// cannot be opened. -func (OSFileSystem) Open(name string) (fs.File, error) { - return os.Open(name) -} - // ParseFileContents takes in a map of strings and any type and returns a DeploymentParameters object and // an error if one occurs during the process. func (pp ParameterParser) ParseFileContents(input map[string]any) (clients.DeploymentParameters, error) { @@ -90,7 +81,7 @@ func (pp ParameterParser) parseSingle(input string, output clients.DeploymentPar if strings.HasPrefix(input, "@") { // input is a file that declares multiple parameters filePath := strings.TrimPrefix(input, "@") - b, err := fs.ReadFile(pp.FileSystem, filePath) + b, err := afero.ReadFile(pp.FileSystem, filePath) if err != nil { return err } @@ -111,7 +102,7 @@ func (pp ParameterParser) parseSingle(input string, output clients.DeploymentPar if strings.HasPrefix(parameterValue, "@") { // input is a file that declares a single parameter filePath := strings.TrimPrefix(parameterValue, "@") - b, err := fs.ReadFile(pp.FileSystem, filePath) + b, err := afero.ReadFile(pp.FileSystem, filePath) if err != nil { return err } diff --git a/pkg/cli/bicep/deployment_parameters_test.go b/pkg/cli/bicep/deployment_parameters_test.go index f348240984..e355aa13f9 100644 --- a/pkg/cli/bicep/deployment_parameters_test.go +++ b/pkg/cli/bicep/deployment_parameters_test.go @@ -21,7 +21,8 @@ import ( "os" "path/filepath" "testing" - "testing/fstest" + + "github.com/spf13/afero" "github.com/radius-project/radius/pkg/cli/clients" "github.com/stretchr/testify/require" @@ -36,7 +37,7 @@ func Test_Parameters_Invalid(t *testing.T) { } parser := ParameterParser{ - FileSystem: fstest.MapFS{}, + FileSystem: afero.NewMemMapFs(), } for _, input := range inputs { @@ -56,15 +57,24 @@ func Test_ParseParameters_Overwrite(t *testing.T) { "key3=value3", } + // Initialize the in-memory filesystem + fs := afero.NewMemMapFs() + + // Create the "many.json" file with the specified content + err := afero.WriteFile(fs, "many.json", []byte(`{ "parameters": { "key1": { "value": { "someValue": true } }, "key2": { "value": "overridden-value" } } }`), 0644) + if err != nil { + t.Fatalf("Failed to create many.json: %v", err) + } + + // Create the "single.json" file with the specified content + err = afero.WriteFile(fs, "single.json", []byte(`{ "someValue": "another-value" }`), 0644) + if err != nil { + t.Fatalf("Failed to create single.json: %v", err) + } + + // Initialize the ParameterParser with the in-memory filesystem parser := ParameterParser{ - FileSystem: fstest.MapFS{ - "many.json": { - Data: []byte(`{ "parameters": { "key1": { "value": { "someValue": true } }, "key2": { "value": "overridden-value" } } }`), - }, - "single.json": { - Data: []byte(`{ "someValue": "another-value" }`), - }, - }, + FileSystem: fs, } parameters, err := parser.Parse(inputs...) @@ -91,7 +101,7 @@ func Test_ParseParameters_Overwrite(t *testing.T) { func Test_ParseParameters_File(t *testing.T) { parser := ParameterParser{ - FileSystem: fstest.MapFS{}, + FileSystem: afero.NewMemMapFs(), } input, err := os.ReadFile(filepath.Join("testdata", "test-parameters.json")) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go new file mode 100644 index 0000000000..340c156226 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -0,0 +1,405 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bicep + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/afero" + + "github.com/radius-project/radius/pkg/cli" + "github.com/radius-project/radius/pkg/cli/bicep" + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/clierrors" + "github.com/radius-project/radius/pkg/cli/cmd/commonflags" + "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/deploy" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" + "gopkg.in/yaml.v2" +) + +// NewCommand creates a command for the `rad bicep generate-kubernetes-manifest` command. +func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { + runner := NewRunner(factory) + + cmd := &cobra.Command{ + Use: "generate-kubernetes-manifest [file]", + Short: "Generate a DeploymentTemplate Custom Resource.", + Long: `Generate a DeploymentTemplate Custom Resource. + + This command compiles a Bicep template with the given parameters and outputs a DeploymentTemplate Custom Resource. + + You can specify parameters using the '--parameter' flag ('-p' for short). Parameters can be passed as: + + - A file containing multiple parameters using the ARM JSON parameter format (see below) + - A file containing a single value in JSON format + - A key-value-pair passed in the command line + + When passing multiple parameters in a single file, use the format described here: + + https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/parameter-files + + You can specify parameters using multiple sources. Parameters can be overridden based on the + order the are provided. Parameters appearing later in the argument list will override those defined earlier. + `, + Example: ` +# Generate a DeploymentTemplate Custom Resource from a Bicep file. +rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam --parameters tag=latest --outfile app.yaml + `, + Args: cobra.ExactArgs(1), + RunE: framework.RunCommand(runner), + } + + commonflags.AddWorkspaceFlag(cmd) + commonflags.AddResourceGroupFlag(cmd) + commonflags.AddEnvironmentNameFlag(cmd) + commonflags.AddApplicationNameFlag(cmd) + commonflags.AddParameterFlag(cmd) + + cmd.Flags().String("outfile", "", "Path of the generated DeploymentTemplate yaml file.") + _ = cmd.MarkFlagFilename("outfile", ".yaml") + + return cmd, runner +} + +// Runner is the runner implementation for the `rad bicep generate-kubernetes` command. +type Runner struct { + Bicep bicep.Interface + ConfigHolder *framework.ConfigHolder + ConnectionFactory connections.Factory + Deploy deploy.Interface + Output output.Interface + + FileSystem afero.Fs + EnvironmentNameOrID string + FilePath string + Parameters map[string]map[string]any + Workspace *workspaces.Workspace + Providers *clients.Providers + OutFile string +} + +// NewRunner creates a new instance of the `rad deploy` runner. +func NewRunner(factory framework.Factory) *Runner { + return &Runner{ + Bicep: factory.GetBicep(), + ConnectionFactory: factory.GetConnectionFactory(), + ConfigHolder: factory.GetConfigHolder(), + Deploy: factory.GetDeploy(), + Output: factory.GetOutput(), + } +} + +// Validate validates the inputs of the rad bicep generate-kubernetes-manifest command. +func (r *Runner) Validate(cmd *cobra.Command, args []string) error { + workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + if err != nil { + return err + } + + r.Workspace = workspace + + // Allow --group to override the scope + scope, err := cli.RequireScope(cmd, *workspace) + if err != nil { + return err + } + + // We don't need to explicitly validate the existence of the scope, because we'll validate the existence + // of the environment later. That will give an appropriate error message for the case where the group + // does not exist. + workspace.Scope = scope + + r.EnvironmentNameOrID, err = cli.RequireEnvironmentNameOrID(cmd, args, *workspace) + if err != nil { + return err + } + + // Validate that the environment exists. + // Right now we assume that every deployment uses a Radius Environment. + client, err := r.ConnectionFactory.CreateApplicationsManagementClient(cmd.Context(), *r.Workspace) + if err != nil { + return err + } + env, err := client.GetEnvironment(cmd.Context(), r.EnvironmentNameOrID) + if err != nil { + // If the error is not a 404, return it + if !clients.Is404Error(err) { + return err + } + + // If the environment doesn't exist, but the user specified its name or resource id as + // a command-line option, return an error + if cli.DidSpecifyEnvironmentName(cmd, args) { + return clierrors.Message("The environment %q does not exist in scope %q. Run `rad env create` first. You could also provide the environment ID if the environment exists in a different group.", r.EnvironmentNameOrID, r.Workspace.Scope) + } + + // If we got here, it means that the error was a 404 and the user did not specify the environment name. + // This is fine, because an environment is not required. + } + + r.Providers = &clients.Providers{} + r.Providers.Radius = &clients.RadiusProvider{} + if env.ID != nil { + r.Providers.Radius.EnvironmentID = *env.ID + r.Workspace.Environment = r.Providers.Radius.EnvironmentID + } + + if env.Properties != nil && env.Properties.Providers != nil { + if env.Properties.Providers.Aws != nil { + r.Providers.AWS = &clients.AWSProvider{ + Scope: *env.Properties.Providers.Aws.Scope, + } + } + if env.Properties.Providers.Azure != nil { + r.Providers.Azure = &clients.AzureProvider{ + Scope: *env.Properties.Providers.Azure.Scope, + } + } + } + + r.FilePath = args[0] + + parameterArgs, err := cmd.Flags().GetStringArray("parameters") + if err != nil { + return err + } + + if r.FileSystem == nil { + r.FileSystem = afero.NewOsFs() + } + + parser := bicep.ParameterParser{FileSystem: r.FileSystem} + r.Parameters, err = parser.Parse(parameterArgs...) + if err != nil { + return err + } + + return nil +} + +// Run runs the rad bicep generate-kubernetes-manifest command. +func (r *Runner) Run(ctx context.Context) error { + template, err := r.Bicep.PrepareTemplate(r.FilePath) + if err != nil { + return err + } + + // This is the earliest point where we can inject parameters, we have + // to wait until the template is prepared. + err = r.injectAutomaticParameters(template) + if err != nil { + return err + } + + // This is the earliest point where we can report missing parameters, we have + // to wait until the template is prepared. + err = r.reportMissingParameters(template) + if err != nil { + return err + } + + // create a DeploymentTemplate yaml file + // with the basefilename from the bicepfile + if r.OutFile == "" { + r.OutFile = strings.TrimSuffix(filepath.Base(r.FilePath), filepath.Ext(r.FilePath)) + ".yaml" + } + + deploymentTemplate, err := r.generateDeploymentTemplate(r.OutFile, template, r.Parameters, r.Providers) + if err != nil { + return err + } + + err = r.createDeploymentTemplateYAMLFile(deploymentTemplate) + if err != nil { + return err + } + + // Print the path to the file + r.Output.LogInfo("DeploymentTemplate file created at %s", r.OutFile) + + return nil +} + +func (r *Runner) injectAutomaticParameters(template map[string]any) error { + if r.Providers.Radius.EnvironmentID != "" { + err := bicep.InjectEnvironmentParam(template, r.Parameters, r.Providers.Radius.EnvironmentID) + if err != nil { + return err + } + } + + return nil +} + +func (r *Runner) reportMissingParameters(template map[string]any) error { + declaredParameters, err := bicep.ExtractParameters(template) + if err != nil { + return err + } + + errors := map[string]string{} + for parameter := range declaredParameters { + // Case-invariant lookup on the user-provided values + match := false + for provided := range r.Parameters { + if strings.EqualFold(parameter, provided) { + match = true + break + } + } + + if match { + // Has user-provided value + continue + } + + if _, ok := bicep.DefaultValue(declaredParameters[parameter]); ok { + // Has default value + continue + } + + // Special case the parameters that are automatically injected + if strings.EqualFold(parameter, "environment") { + errors[parameter] = "The template requires an environment. Use --environment to specify the environment name." + } else { + errors[parameter] = fmt.Sprintf("The template requires a parameter %q. Use --parameters %s= to specify the value.", parameter, parameter) + } + } + + if len(errors) == 0 { + return nil + } + + keys := maps.Keys(errors) + sort.Strings(keys) + + details := []string{} + for _, key := range keys { + details = append(details, fmt.Sprintf(" - %v", errors[key])) + } + + return clierrors.Message("The template %q could not be deployed because of the following errors:\n\n%v", r.FilePath, strings.Join(details, "\n")) +} + +// generateDeploymentTemplate generates a DeploymentTemplate Custom Resource from the given template and parameters. +func (r *Runner) generateDeploymentTemplate(fileName string, template map[string]any, parameters map[string]map[string]any, providers *clients.Providers) (map[string]any, error) { + marshalledTemplate, err := json.Marshal(template) + if err != nil { + return nil, err + } + + marshalledParameters, err := json.Marshal(parameters) + if err != nil { + return nil, err + } + + providerConfig := r.convertProvidersToProviderConfig(providers) + + marshalledProviderConfig, err := json.Marshal(providerConfig) + if err != nil { + return nil, err + } + + deploymentTemplate := map[string]any{ + "kind": "DeploymentTemplate", + "apiVersion": "radapp.io/v1alpha3", + "metadata": map[string]any{ + "name": fileName, + "namespace": "radius-system", + }, + "spec": map[string]any{ + "template": string(marshalledTemplate), + "parameters": string(marshalledParameters), + "providerConfig": string(marshalledProviderConfig), + }, + } + + return deploymentTemplate, nil +} + +// createDeploymentTemplateYAMLFile creates a DeploymentTemplate YAML file with the given content. +func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string]any) error { + fmt.Println("Creating DeploymentTemplate YAML file") + f, err := r.FileSystem.Create(r.OutFile) + if err != nil { + return err + } + + defer f.Close() + + deploymentTemplateYaml, err := yaml.Marshal(deploymentTemplate) + if err != nil { + return err + } + + _, err = f.Write(deploymentTemplateYaml) + if err != nil { + return err + } + + return nil +} + +// convertProvidersToProviderConfig converts the the clients.Providers to sdkclients.ProviderConfig. +func (r *Runner) convertProvidersToProviderConfig(providers *clients.Providers) (providerConfig sdkclients.ProviderConfig) { + providerConfig = sdkclients.ProviderConfig{} + if providers != nil { + if providers.AWS != nil { + providerConfig.AWS = &sdkclients.AWS{ + Type: "aws", + Value: sdkclients.Value{ + Scope: providers.AWS.Scope, + }, + } + } + if providers.Azure != nil { + providerConfig.Az = &sdkclients.Az{ + Type: "azure", + Value: sdkclients.Value{ + Scope: providers.Azure.Scope, + }, + } + } + if providers.Radius != nil { + providerConfig.Radius = &sdkclients.Radius{ + Type: "radius", + Value: sdkclients.Value{ + Scope: r.Workspace.Scope, + }, + } + providerConfig.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: r.Workspace.Scope, + }, + } + } + } + + return providerConfig +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go new file mode 100644 index 0000000000..244fd3f443 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go @@ -0,0 +1,460 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bicep + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/radius-project/radius/pkg/cli/bicep" + "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/framework" + "github.com/radius-project/radius/pkg/cli/output" + "github.com/radius-project/radius/pkg/cli/workspaces" + "github.com/radius-project/radius/test/radcli" + "github.com/spf13/afero" + + "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + "github.com/radius-project/radius/pkg/to" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func Test_CommandValidation(t *testing.T) { + radcli.SharedCommandValidation(t, NewCommand) +} + +func Test_Validate(t *testing.T) { + configWithWorkspace := radcli.LoadConfigWithWorkspace(t) + testcases := []radcli.ValidateInput{ + + { + Name: "rad bicep generate-kubernetes-manifest - valid", + Input: []string{"app.bicep"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/Applications.Core/environments/test-environment"). + Return(v20231001preview.EnvironmentResource{}, nil). + Times(1) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - valid with parameters", + Input: []string{"app.bicep", "-p", "foo=bar", "--parameters", "a=b"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), radcli.TestEnvironmentID). + Return(v20231001preview.EnvironmentResource{}, nil). + Times(1) + + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - valid with environment", + Input: []string{"app.bicep", "-e", "prod"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "prod"). + Return(v20231001preview.EnvironmentResource{ + Properties: &v20231001preview.EnvironmentProperties{ + Providers: &v20231001preview.Providers{ + Azure: &v20231001preview.ProvidersAzure{ + Scope: to.Ptr("/subscriptions/test-subId/resourceGroups/test-rg"), + }, + }, + }, + }, nil). + Times(1) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - env does not exist invalid", + Input: []string{"app.bicep", "-e", "prod"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "prod"). + Return(v20231001preview.EnvironmentResource{}, radcli.Create404Error()). + Times(1) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - valid with env ID", + Input: []string{"app.bicep", "-e", "/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"). + Return(v20231001preview.EnvironmentResource{ + ID: to.Ptr("/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"), + }, nil). + Times(1) + }, + ValidateCallback: func(t *testing.T, obj framework.Runner) { + runner := obj.(*Runner) + scope := "/planes/radius/local/resourceGroups/test-resource-group" + environmentID := scope + "/providers/applications.core/environments/prod" + require.Equal(t, scope, runner.Workspace.Scope) + require.Equal(t, environmentID, runner.Workspace.Environment) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - fallback workspace", + Input: []string{"app.bicep", "--group", "my-group", "--environment", "prod"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: radcli.LoadEmptyConfig(t), + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "prod"). + Return(v20231001preview.EnvironmentResource{}, nil). + Times(1) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - fallback workspace requires resource group", + Input: []string{"app.bicep", "--environment", "prod"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: radcli.LoadEmptyConfig(t), + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - too many args", + Input: []string{"app.bicep", "anotherfile.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: radcli.LoadEmptyConfig(t), + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - valid with outfile", + Input: []string{"app.bicep", "--outfile", "test.yaml"}, + ExpectedValid: true, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: configWithWorkspace, + }, + ConfigureMocks: func(mocks radcli.ValidateMocks) { + mocks.ApplicationManagementClient.EXPECT(). + GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/Applications.Core/environments/test-environment"). + Return(v20231001preview.EnvironmentResource{}, nil). + Times(1) + }, + }, + { + Name: "rad bicep generate-kubernetes-manifest - invalid outfile", + Input: []string{"app.bicep", "anotherfile.json"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: radcli.LoadEmptyConfig(t), + }, + }, + } + + radcli.SharedValidateValidation(t, NewCommand, testcases) +} + +func Test_Run(t *testing.T) { + t.Run("Create basic DeploymentTemplate", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bicep := bicep.NewMockInterface(ctrl) + bicep.EXPECT(). + PrepareTemplate("basic.bicep"). + Return(map[string]any{}, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Scope: "/planes/radius/local/resourceGroups/test-resource-group", + Name: "kind-kind", + } + provider := &clients.Providers{ + Radius: &clients.RadiusProvider{ + EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), + }, + } + + filePath := "basic.bicep" + + outputSink := &output.MockOutput{} + runner := &Runner{ + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + Workspace: workspace, + Providers: provider, + FileSystem: afero.NewMemMapFs(), + } + + fileExists, err := afero.Exists(runner.FileSystem, "basic.yaml") + require.NoError(t, err) + require.False(t, fileExists) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + fileExists, err = afero.Exists(runner.FileSystem, "basic.yaml") + require.NoError(t, err) + require.True(t, fileExists) + + require.Equal(t, "basic.yaml", runner.OutFile) + + expected, err := os.ReadFile(filepath.Join("testdata", "basic.yaml")) + require.NoError(t, err) + + // assert that the file contents are as expected + actual, err := afero.ReadFile(runner.FileSystem, "basic.yaml") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + }) + + t.Run("Create DeploymentTemplate with template content", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bicep := bicep.NewMockInterface(ctrl) + bicep.EXPECT(). + PrepareTemplate("value.bicep"). + Return(map[string]any{ + "resources": []map[string]any{ + { + "some-key": "some-value", + }, + }, + "parameters": map[string]any{ + "kubernetesNamespace": map[string]any{}, + }, + }, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Scope: "/planes/radius/local/resourceGroups/test-resource-group", + Name: "kind-kind", + } + provider := &clients.Providers{ + Radius: &clients.RadiusProvider{ + EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), + }, + } + + parameters := map[string]map[string]any{ + "kubernetesNamespace": { + "value": "test-namespace", + }, + } + + filePath := "value.bicep" + + outputSink := &output.MockOutput{} + runner := &Runner{ + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: parameters, + Workspace: workspace, + Providers: provider, + FileSystem: afero.NewMemMapFs(), + } + + fileExists, err := afero.Exists(runner.FileSystem, "value.yaml") + require.NoError(t, err) + require.False(t, fileExists) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + fileExists, err = afero.Exists(runner.FileSystem, "value.yaml") + require.NoError(t, err) + require.True(t, fileExists) + + require.Equal(t, "value.yaml", runner.OutFile) + + expected, err := os.ReadFile(filepath.Join("testdata", "value.yaml")) + require.NoError(t, err) + + // assert that the file contents are as expected + actual, err := afero.ReadFile(runner.FileSystem, "value.yaml") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + }) + + t.Run("Create DeploymentTemplate with Azure scope", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bicep := bicep.NewMockInterface(ctrl) + bicep.EXPECT(). + PrepareTemplate("azure.bicep"). + Return(map[string]any{}, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Scope: "/planes/radius/local/resourceGroups/test-resource-group", + Name: "kind-kind", + } + provider := &clients.Providers{ + Radius: &clients.RadiusProvider{ + EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), + }, + Azure: &clients.AzureProvider{ + Scope: "/subscriptions/test-subId/resourceGroups/test-rg", + }, + } + + filePath := "azure.bicep" + + outputSink := &output.MockOutput{} + runner := &Runner{ + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + Workspace: workspace, + Providers: provider, + FileSystem: afero.NewMemMapFs(), + } + + fileExists, err := afero.Exists(runner.FileSystem, "azure.yaml") + require.NoError(t, err) + require.False(t, fileExists) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + fileExists, err = afero.Exists(runner.FileSystem, "azure.yaml") + require.NoError(t, err) + require.True(t, fileExists) + + require.Equal(t, "azure.yaml", runner.OutFile) + + expected, err := os.ReadFile(filepath.Join("testdata", "azure.yaml")) + require.NoError(t, err) + + // assert that the file contents are as expected + actual, err := afero.ReadFile(runner.FileSystem, "azure.yaml") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + }) + + t.Run("Create DeploymentTemplate with AWS scope", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bicep := bicep.NewMockInterface(ctrl) + bicep.EXPECT(). + PrepareTemplate("aws.bicep"). + Return(map[string]any{}, nil). + Times(1) + + workspace := &workspaces.Workspace{ + Connection: map[string]any{ + "kind": "kubernetes", + "context": "kind-kind", + }, + Scope: "/planes/radius/local/resourceGroups/test-resource-group", + Name: "kind-kind", + } + provider := &clients.Providers{ + Radius: &clients.RadiusProvider{ + EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), + }, + AWS: &clients.AWSProvider{ + Scope: "awsscope", + }, + } + + filePath := "aws.bicep" + + outputSink := &output.MockOutput{} + runner := &Runner{ + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + Workspace: workspace, + Providers: provider, + FileSystem: afero.NewMemMapFs(), + } + + fileExists, err := afero.Exists(runner.FileSystem, "aws.yaml") + require.NoError(t, err) + require.False(t, fileExists) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + fileExists, err = afero.Exists(runner.FileSystem, "aws.yaml") + require.NoError(t, err) + require.True(t, fileExists) + + require.Equal(t, "aws.yaml", runner.OutFile) + + expected, err := os.ReadFile(filepath.Join("testdata", "aws.yaml")) + require.NoError(t, err) + + // assert that the file contents are as expected + actual, err := afero.ReadFile(runner.FileSystem, "aws.yaml") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + }) +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml new file mode 100644 index 0000000000..e3c4a95a4e --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml @@ -0,0 +1,9 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: aws.yaml + namespace: radius-system +spec: + parameters: '{}' + providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"aws":{"type":"aws","value":{"scope":"awsscope"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' + template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml new file mode 100644 index 0000000000..650d876456 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml @@ -0,0 +1,9 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: azure.yaml + namespace: radius-system +spec: + parameters: '{}' + providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"az":{"type":"azure","value":{"scope":"/subscriptions/test-subId/resourceGroups/test-rg"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' + template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml new file mode 100644 index 0000000000..f6760734cb --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml @@ -0,0 +1,9 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: basic.yaml + namespace: radius-system +spec: + parameters: '{}' + providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' + template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml new file mode 100644 index 0000000000..99d24a6dc6 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml @@ -0,0 +1,9 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: value.yaml + namespace: radius-system +spec: + parameters: '{"kubernetesNamespace":{"value":"test-namespace"}}' + providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' + template: '{"parameters":{"kubernetesNamespace":{}},"resources":[{"some-key":"some-value"}]}' diff --git a/pkg/cli/cmd/deploy/deploy.go b/pkg/cli/cmd/deploy/deploy.go index e651d96be6..bc4f0a9df0 100644 --- a/pkg/cli/cmd/deploy/deploy.go +++ b/pkg/cli/cmd/deploy/deploy.go @@ -35,6 +35,7 @@ import ( "github.com/radius-project/radius/pkg/cli/workspaces" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/to" + "github.com/spf13/afero" "github.com/spf13/cobra" "golang.org/x/exp/maps" ) @@ -235,7 +236,7 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } - parser := bicep.ParameterParser{FileSystem: bicep.OSFileSystem{}} + parser := bicep.ParameterParser{FileSystem: afero.NewOsFs()} r.Parameters, err = parser.Parse(parameterArgs...) if err != nil { return err diff --git a/pkg/cli/cmd/deploy/deploy_test.go b/pkg/cli/cmd/deploy/deploy_test.go index 30635aa866..954b5e8a4d 100644 --- a/pkg/cli/cmd/deploy/deploy_test.go +++ b/pkg/cli/cmd/deploy/deploy_test.go @@ -527,7 +527,6 @@ func Test_Run(t *testing.T) { }) t.Run("Deployment with missing parameters", func(t *testing.T) { - //t.Skip() ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/pkg/cli/cmd/recipe/register/register.go b/pkg/cli/cmd/recipe/register/register.go index b12081a16a..ce3c39633d 100644 --- a/pkg/cli/cmd/recipe/register/register.go +++ b/pkg/cli/cmd/recipe/register/register.go @@ -19,6 +19,8 @@ package register import ( "context" + "github.com/spf13/afero" + "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/bicep" "github.com/radius-project/radius/pkg/cli/clierrors" @@ -148,7 +150,7 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } - parser := bicep.ParameterParser{FileSystem: bicep.OSFileSystem{}} + parser := bicep.ParameterParser{FileSystem: afero.NewOsFs()} r.Parameters, err = parser.Parse(parameterArgs...) if err != nil { return err diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 9783d8deec..4b1083c3a2 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "fmt" - "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -31,6 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/go-logr/logr" + "github.com/radius-project/radius/pkg/cli/clients" "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" @@ -161,6 +161,12 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d // If we get here, the operation is complete. _, err = poller.Result(ctx) if err != nil { + if clients.Is404Error(err) { + // The resource was not found, so we can consider it deleted. + logger.Info("Resource was not found.") + return ctrl.Result{}, nil + } + // Operation failed, reset state and retry. r.EventRecorder.Event(deploymentResource, corev1.EventTypeWarning, "ResourceError", err.Error()) logger.Error(err, "Delete failed.") @@ -211,53 +217,53 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // Check other resources that depend on this resource. // List all DeploymentResource objects in the same namespace - deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) - if err != nil { - return ctrl.Result{}, nil - } - - appsCount := 0 - envsCount := 0 - otherCount := 0 - for _, dr := range deploymentResourceList.Items { - if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { - continue - } - if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { - appsCount++ - } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { - envsCount++ - } else if dr.Spec.Id != "" { - logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) - otherCount++ - } - } - - if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { - // dont delete app until otherCount is 0 - if otherCount > 0 { - logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } - } - - if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { - if otherCount > 0 { - logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } - } + // deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} + // err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) + // if err != nil { + // return ctrl.Result{}, nil + // } + + // appsCount := 0 + // envsCount := 0 + // otherCount := 0 + // for _, dr := range deploymentResourceList.Items { + // if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { + // continue + // } + // if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { + // appsCount++ + // } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { + // envsCount++ + // } else if dr.Spec.Id != "" { + // logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) + // otherCount++ + // } + // } + + // if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { + // // dont delete app until otherCount is 0 + // if otherCount > 0 { + // logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + // deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + // err = r.Client.Status().Update(ctx, deploymentResource) + // if err != nil { + // return ctrl.Result{}, err + // } + // return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + // } + // } + + // if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { + // if otherCount > 0 { + // logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + // deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + // err = r.Client.Status().Update(ctx, deploymentResource) + // if err != nil { + // return ctrl.Result{}, err + // } + // return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + // } + // } poller, err := r.startDeleteOperation(ctx, deploymentResource) if err != nil { From 964f7d62060c8a0c756d33da9ee990a4b46bbb68 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 29 Oct 2024 14:45:51 -0700 Subject: [PATCH 10/65] PR Signed-off-by: willdavsmith --- .../deploymentresource_reconciler.go | 95 ++++++++++--------- .../deploymenttemplate_reconciler_test.go | 74 --------------- .../sample-repo/deploymenttemplate/app.yaml | 0 3 files changed, 48 insertions(+), 121 deletions(-) delete mode 100644 test/gitops/sample-repo/deploymenttemplate/app.yaml diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 4b1083c3a2..2ea45e4494 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -217,53 +218,53 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // Check other resources that depend on this resource. // List all DeploymentResource objects in the same namespace - // deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - // err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) - // if err != nil { - // return ctrl.Result{}, nil - // } - - // appsCount := 0 - // envsCount := 0 - // otherCount := 0 - // for _, dr := range deploymentResourceList.Items { - // if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { - // continue - // } - // if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { - // appsCount++ - // } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { - // envsCount++ - // } else if dr.Spec.Id != "" { - // logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) - // otherCount++ - // } - // } - - // if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { - // // dont delete app until otherCount is 0 - // if otherCount > 0 { - // logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - // deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - // err = r.Client.Status().Update(ctx, deploymentResource) - // if err != nil { - // return ctrl.Result{}, err - // } - // return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - // } - // } - - // if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { - // if otherCount > 0 { - // logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - // deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - // err = r.Client.Status().Update(ctx, deploymentResource) - // if err != nil { - // return ctrl.Result{}, err - // } - // return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - // } - // } + deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} + err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) + if err != nil { + return ctrl.Result{}, nil + } + + appsCount := 0 + envsCount := 0 + otherCount := 0 + for _, dr := range deploymentResourceList.Items { + if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { + continue + } + if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { + appsCount++ + } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { + envsCount++ + } else if dr.Spec.Id != "" { + logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) + otherCount++ + } + } + + if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { + // dont delete app until otherCount is 0 + if otherCount > 0 { + logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + } + + if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { + if otherCount > 0 { + logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil + } + } poller, err := r.startDeleteOperation(ctx, deploymentResource) if err != nil { diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 713852f393..a9b4bbbb46 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -148,80 +148,6 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { waitForDeploymentTemplateDeleted(t, client, name) } -func Test_DeploymentTemplateReconciler_ChangeEnvironmentAndApplication(t *testing.T) { - ctx := testcontext.New(t) - radius, client := SetupDeploymentTemplateTest(t) - - name := types.NamespacedName{Namespace: "DeploymentTemplate-change", Name: "test-DeploymentTemplate-change"} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) - require.NoError(t, err) - - deployment := makeDeploymentTemplate(name, map[string]any{}) - err = client.Create(ctx, deployment) - require.NoError(t, err) - - // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") - - // Deployment will be waiting for template to complete provisioning. - status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - scope, err := parseDeploymentScopeFromProviderConfig(status.ProviderConfig) - require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) - - radius.CompleteOperation(status.Operation.ResumeToken, nil) - - // Deployment will update after operation completes - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-change/providers/Microsoft.Resources/deployments/test-DeploymentTemplate-change", status.Resource) - - _, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) - - createEnvironment(radius, "new-environment") - - // Now update the deployment to change the environment and application. - err = client.Get(ctx, name, deployment) - require.NoError(t, err) - - err = client.Update(ctx, deployment) - require.NoError(t, err) - - // Now the deployment will delete and re-create all of the items - - // Deletion of the deployment is in progress. - status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) - scope, err = parseDeploymentScopeFromProviderConfig(status.ProviderConfig) - require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) - - radius.CompleteOperation(status.Operation.ResumeToken, nil) - - // Resource should be gone. - _, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.Error(t, err) - - // Deployment will be waiting for extender to complete provisioning. - status = waitForDeploymentTemplateStateUpdating(t, client, name, nil) - require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application", scope) - radius.CompleteOperation(status.Operation.ResumeToken, nil) - - // Deployment will update after operation completes - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/new-environment-new-application/providers/Microsoft.Resources/deployments/test-DeploymentTemplate-change", status.Resource) - - // Now delete the deployment. - err = client.Delete(ctx, deployment) - require.NoError(t, err) - - // Deletion of the resource is in progress. - status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) - radius.CompleteOperation(status.Operation.ResumeToken, nil) - - // Now deleting of the deployment object can complete. - waitForDeploymentTemplateDeleted(t, client, name) -} - func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { // This test tests our ability to recover from failed operations inside Radius. // diff --git a/test/gitops/sample-repo/deploymenttemplate/app.yaml b/test/gitops/sample-repo/deploymenttemplate/app.yaml deleted file mode 100644 index e69de29bb2..0000000000 From 2b31fb7a5b4d55f955858fa6f113052467213b71 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 29 Oct 2024 15:03:28 -0700 Subject: [PATCH 11/65] removing fluxcontroller Signed-off-by: willdavsmith --- build/build.mk | 9 - build/docker.mk | 3 +- .../templates/controller/deployment.yaml | 11 - deploy/Chart/values.yaml | 5 - deploy/images/bicep/Dockerfile | 12 - deploy/images/controller/Dockerfile | 13 +- go.mod | 22 +- go.sum | 31 +- .../deploymentresource_reconciler.go | 4 + .../reconciler/gitrepository_predicate.go | 67 ---- .../gitrepository_predicate_test.go | 212 ------------ .../reconciler/gitrepository_watcher.go | 307 ------------------ .../reconciler/gitrepository_watcher_test.go | 24 -- pkg/controller/service.go | 10 - 14 files changed, 31 insertions(+), 699 deletions(-) delete mode 100644 deploy/images/bicep/Dockerfile delete mode 100644 pkg/controller/reconciler/gitrepository_predicate.go delete mode 100644 pkg/controller/reconciler/gitrepository_predicate_test.go delete mode 100644 pkg/controller/reconciler/gitrepository_watcher.go delete mode 100644 pkg/controller/reconciler/gitrepository_watcher_test.go diff --git a/build/build.mk b/build/build.mk index 2ecc06a3c3..7f4016cfdb 100644 --- a/build/build.mk +++ b/build/build.mk @@ -153,12 +153,3 @@ clean: ## Cleans output directory. .PHONY: lint lint: ## Runs golangci-lint $(GOLANGCI_LINT) run --fix --timeout 5m - -define generateBicepBuildTarget -.PHONY: build-bicep-$(1)-$(2) -build-bicep-$(1)-$(2): - @echo "$(ARROW) Building bicep on $(1)/$(2)" -endef - -# Generate bicep build targets for each combination of OS and ARCH -$(foreach ARCH,$(GOARCHES),$(foreach OS,$(GOOSES),$(eval $(call generateBicepBuildTarget,$(OS),$(ARCH))))) diff --git a/build/docker.mk b/build/docker.mk index 24fa602585..7adcdcaa6b 100644 --- a/build/docker.mk +++ b/build/docker.mk @@ -104,8 +104,7 @@ APPS_MAP := ucpd:./deploy/images/ucpd \ dynamic-rp:./deploy/images/dynamic-rp \ controller:./deploy/images/controller \ testrp:./test/testrp \ - magpiego:./test/magpiego \ - bicep:./deploy/images/bicep + magpiego:./test/magpiego # Function to extract the name and the directory of the Dockerfile from the app string define parseApp diff --git a/deploy/Chart/templates/controller/deployment.yaml b/deploy/Chart/templates/controller/deployment.yaml index c4ba47a6ac..c055a7dc3f 100644 --- a/deploy/Chart/templates/controller/deployment.yaml +++ b/deploy/Chart/templates/controller/deployment.yaml @@ -27,13 +27,6 @@ spec: {{- end }} spec: serviceAccountName: controller - initContainers: - - name: bicep - image: "{{ .Values.bicep.image }}:{{ .Values.bicep.tag | default $appversion }}" - command: ['sh', '-c', 'mv ./bicep /usr/local/bin/bicep'] - volumeMounts: - - name: bicep - mountPath: /usr/local/bin containers: - name: controller image: "{{ .Values.controller.image }}:{{ .Values.controller.tag | default $appversion }}" @@ -69,8 +62,6 @@ spec: resources:{{ toYaml .Values.controller.resources | nindent 10 }} {{- end }} volumeMounts: - - name: bicep - mountPath: /usr/local/bin - name: config-volume mountPath: /etc/config - name: cert @@ -82,8 +73,6 @@ spec: readOnly: true {{- end }} volumes: - - name: bicep - emptyDir: {} - name: config-volume configMap: name: controller-config diff --git a/deploy/Chart/values.yaml b/deploy/Chart/values.yaml index be39fdaf7d..d750295570 100644 --- a/deploy/Chart/values.yaml +++ b/deploy/Chart/values.yaml @@ -112,8 +112,3 @@ dashboard: memory: "60Mi" limits: memory: "300Mi" - -bicep: - image: ghcr.io/radius-project/bicep - # Default tag uses Chart AppVersion. - # tag: latest diff --git a/deploy/images/bicep/Dockerfile b/deploy/images/bicep/Dockerfile deleted file mode 100644 index cb0657a98e..0000000000 --- a/deploy/images/bicep/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM alpine:latest - -ARG TARGETARCH - -RUN apk --no-cache add curl - -WORKDIR / - -RUN curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x64 \ - && chmod +x ./bicep - -ENTRYPOINT ["/bin/sh"] diff --git a/deploy/images/controller/Dockerfile b/deploy/images/controller/Dockerfile index 72b1ed8284..739b1d607e 100644 --- a/deploy/images/controller/Dockerfile +++ b/deploy/images/controller/Dockerfile @@ -1,4 +1,5 @@ -FROM ubuntu:latest +# Use distroless image which already includes ca-certificates +FROM gcr.io/distroless/static:nonroot # Argument for target architecture ARG TARGETARCH @@ -6,15 +7,11 @@ ARG TARGETARCH # Set the working directory WORKDIR / -# Install the required dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - libicu-dev \ - ca-certificates && \ - rm -rf /var/lib/apt/lists/* - # Copy the application binary for the specified architecture COPY ./linux_${TARGETARCH:-amd64}/release/controller / +# Set the user to non-root (65532:65532 is the default non-root user in distroless) +USER 65532:65532 + # Set the entrypoint to the application binary ENTRYPOINT ["/controller"] diff --git a/go.mod b/go.mod index dab7a101cb..e454a9c688 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,6 @@ module github.com/radius-project/radius go 1.23.0 -// Replace digest lib to master to gather access to BLAKE3. -// xref: https://github.com/opencontainers/go-digest/pull/66 -replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be - require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 @@ -38,10 +34,13 @@ require ( github.com/fatih/color v1.18.0 ======= github.com/fatih/color v1.17.0 +<<<<<<< HEAD github.com/fluxcd/pkg/http/fetch v0.12.1 github.com/fluxcd/pkg/tar v0.8.1 github.com/fluxcd/source-controller/api v1.4.1 >>>>>>> 88eea07d2 (PR) +======= +>>>>>>> ff7b44061 (removing fluxcontroller) github.com/go-chi/chi/v5 v5.1.0 github.com/go-git/go-git/v5 v5.12.0 github.com/go-logr/logr v1.4.2 @@ -132,8 +131,6 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect - github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect @@ -149,11 +146,11 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 // indirect + github.com/onsi/ginkgo/v2 v2.20.1 // indirect + github.com/onsi/gomega v1.34.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -167,7 +164,6 @@ require ( github.com/ulikunitz/xz v0.5.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - github.com/zeebo/blake3 v0.2.3 // indirect go.mongodb.org/mongo-driver v1.15.1 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/tools v0.25.0 // indirect @@ -307,7 +303,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/afero v1.11.0 github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect @@ -345,9 +341,15 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect +<<<<<<< HEAD gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiserver v0.31.2 // indirect k8s.io/component-base v0.31.2 // indirect +======= + gopkg.in/yaml.v2 v2.4.0 + k8s.io/apiserver v0.31.1 // indirect + k8s.io/component-base v0.31.1 // indirect +>>>>>>> ff7b44061 (removing fluxcontroller) k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect diff --git a/go.sum b/go.sum index dd9ebd5b7a..67b8320507 100644 --- a/go.sum +++ b/go.sum @@ -469,18 +469,6 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/pkg/apis/acl v0.3.0 h1:UOrKkBTOJK+OlZX7n8rWt2rdBmDCoTK+f5TY2LcZi8A= -github.com/fluxcd/pkg/apis/acl v0.3.0/go.mod h1:WVF9XjSMVBZuU+HTTiSebGAWMgM7IYexFLyVWbK9bNY= -github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0= -github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280= -github.com/fluxcd/pkg/http/fetch v0.12.1 h1:Iap/cdKols3fW39/MyTGqNXHglaA1FJsWtFgYG2hbCQ= -github.com/fluxcd/pkg/http/fetch v0.12.1/go.mod h1:t3JL+uqJ46Wm0CwVRn6Pf/3kOqh45tMoR0pMxLhextQ= -github.com/fluxcd/pkg/tar v0.8.1 h1:K9RWV+E/+Qbz6Mzcg+S9DkVvZrWwJq4957Kqms183RQ= -github.com/fluxcd/pkg/tar v0.8.1/go.mod h1:vuGrnXQPcdi3M4DoVtwvAyvLnSeFgXRJckTGYuZOy2Q= -github.com/fluxcd/pkg/testserver v0.7.0 h1:kNVAn+3bAF2rfR9cT6SxzgEz2o84i+o7zKY3XRKTXmk= -github.com/fluxcd/pkg/testserver v0.7.0/go.mod h1:Ih5IK3Y5G3+a6c77BTqFkdPDCY1Yj1A1W5cXQqkCs9s= -github.com/fluxcd/source-controller/api v1.4.1 h1:zV01D7xzHOXWbYXr36lXHWWYS7POARsjLt61Nbh3kVY= -github.com/fluxcd/source-controller/api v1.4.1/go.mod h1:gSjg57T+IG66SsBR0aquv+DFrm4YyBNpKIJVDnu3Ya8= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -786,9 +774,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= -github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -879,6 +864,7 @@ github.com/novln/docker-parser v1.0.0 h1:PjEBd9QnKixcWczNGyEdfUrP6GR0YUilAqG7Wks github.com/novln/docker-parser v1.0.0/go.mod h1:oCeM32fsoUwkwByB5wVjsrsVQySzPWkl3JdlTn1txpE= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +<<<<<<< HEAD github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= <<<<<<< HEAD github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= @@ -897,6 +883,14 @@ github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod >>>>>>> 88eea07d2 (PR) github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 h1:LTxrNWOPwquJy9Cu3oz6QHJIO5M5gNyOZtSybXdyLA4= github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= +======= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +>>>>>>> ff7b44061 (removing fluxcontroller) github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1062,12 +1056,6 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= -github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= -github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= -github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= -github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= -github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w= @@ -1358,7 +1346,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 2ea45e4494..3603839d79 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -39,6 +39,10 @@ import ( corev1 "k8s.io/api/core/v1" ) +const ( + repositoryField = "spec.repository" +) + // DeploymentResourceReconciler reconciles a DeploymentResource object. type DeploymentResourceReconciler struct { // Client is the Kubernetes client. diff --git a/pkg/controller/reconciler/gitrepository_predicate.go b/pkg/controller/reconciler/gitrepository_predicate.go deleted file mode 100644 index 35e4b2fced..0000000000 --- a/pkg/controller/reconciler/gitrepository_predicate.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2024 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconciler - -import ( - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - sourcev1 "github.com/fluxcd/source-controller/api/v1" -) - -// GitRepositoryRevisionChangePredicate triggers an update event -// when a GitRepository revision changes. -type GitRepositoryRevisionChangePredicate struct { - predicate.Funcs -} - -func (GitRepositoryRevisionChangePredicate) Create(e event.CreateEvent) bool { - src, ok := e.Object.(sourcev1.Source) - - if !ok || src.GetArtifact() == nil { - return false - } - - return true -} - -func (GitRepositoryRevisionChangePredicate) Update(e event.UpdateEvent) bool { - if e.ObjectOld == nil || e.ObjectNew == nil { - return false - } - - oldSource, ok := e.ObjectOld.(sourcev1.Source) - if !ok { - return false - } - - newSource, ok := e.ObjectNew.(sourcev1.Source) - if !ok { - return false - } - - if oldSource.GetArtifact() == nil && newSource.GetArtifact() != nil { - return true - } - - if oldSource.GetArtifact() != nil && newSource.GetArtifact() != nil && - oldSource.GetArtifact().Revision != newSource.GetArtifact().Revision { - return true - } - - return false -} diff --git a/pkg/controller/reconciler/gitrepository_predicate_test.go b/pkg/controller/reconciler/gitrepository_predicate_test.go deleted file mode 100644 index e593785a23..0000000000 --- a/pkg/controller/reconciler/gitrepository_predicate_test.go +++ /dev/null @@ -1,212 +0,0 @@ -/* -Copyright 2024 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconciler - -import ( - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/event" - - sourcev1 "github.com/fluxcd/source-controller/api/v1" -) - -func TestGitRepositoryRevisionChangePredicate_Create(t *testing.T) { - predicate := GitRepositoryRevisionChangePredicate{} - - tests := []struct { - name string - event event.CreateEvent - expected bool - }{ - { - name: "Source is not a sourcev1.Source", - event: event.CreateEvent{ - Object: &corev1.Pod{}, - }, - expected: false, - }, - { - name: "Source has no artifact", - event: event.CreateEvent{ - Object: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - }, - expected: false, - }, - { - name: "Source has an artifact", - event: event.CreateEvent{ - Object: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Status: sourcev1.GitRepositoryStatus{ - Artifact: &sourcev1.Artifact{ - Path: "test-path", - }, - }, - }, - }, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := predicate.Create(tt.event) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestGitRepositoryRevisionChangePredicate_Update(t *testing.T) { - predicate := GitRepositoryRevisionChangePredicate{} - - tests := []struct { - name string - event event.UpdateEvent - expected bool - }{ - { - name: "Source ObjectOld is nil", - event: event.UpdateEvent{ - ObjectNew: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - }, - expected: false, - }, - { - name: "Source ObjectNew is nil", - event: event.UpdateEvent{ - ObjectOld: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - }, - expected: false, - }, - { - name: "Source ObjectOld is not a sourcev1.Source", - event: event.UpdateEvent{ - ObjectOld: &corev1.Pod{}, - ObjectNew: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - }, - expected: false, - }, - { - name: "Source ObjectNew is not a sourcev1.Source", - event: event.UpdateEvent{ - ObjectOld: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - ObjectNew: &corev1.Pod{}, - }, - expected: false, - }, - { - name: "Sources ObjectOld and ObjectNew have no artifact", - event: event.UpdateEvent{ - ObjectOld: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - ObjectNew: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - }, - }, - expected: true, - }, - { - name: "Source ObjectNew and ObjectOld are the same", - event: event.UpdateEvent{ - ObjectOld: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Status: sourcev1.GitRepositoryStatus{ - Artifact: &sourcev1.Artifact{ - Path: "test-path", - }, - }, - }, - ObjectNew: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Status: sourcev1.GitRepositoryStatus{ - Artifact: &sourcev1.Artifact{ - Path: "test-path", - }, - }, - }, - }, - expected: true, - }, - { - name: "Source ObjectNew and ObjectOld are the different", - event: event.UpdateEvent{ - ObjectOld: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Status: sourcev1.GitRepositoryStatus{ - Artifact: &sourcev1.Artifact{ - Path: "test-path", - }, - }, - }, - ObjectNew: &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - }, - Status: sourcev1.GitRepositoryStatus{ - Artifact: &sourcev1.Artifact{ - Path: "test-path-different", - }, - }, - }, - }, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := predicate.Update(tt.event) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/pkg/controller/reconciler/gitrepository_watcher.go b/pkg/controller/reconciler/gitrepository_watcher.go deleted file mode 100644 index cd4eaf7bca..0000000000 --- a/pkg/controller/reconciler/gitrepository_watcher.go +++ /dev/null @@ -1,307 +0,0 @@ -package reconciler - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path" - "strings" - - "github.com/go-logr/logr" - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" - "github.com/radius-project/radius/pkg/ucp/ucplog" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/fluxcd/pkg/http/fetch" - "github.com/fluxcd/pkg/tar" - sourcev1 "github.com/fluxcd/source-controller/api/v1" - - radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" -) - -const ( - repositoryField = "spec.repository" - previousArtifactAnnotation = "previous-artifact" -) - -// GitRepositoryWatcher watches GitRepository objects for revision changes -type GitRepositoryWatcher struct { - client.Client - artifactFetcher *fetch.ArchiveFetcher - HttpRetry int -} - -func (r *GitRepositoryWatcher) SetupWithManager(mgr ctrl.Manager) error { - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentTemplate{}, repositoryField, repositoryIndexer); err != nil { - return err - } - - r.artifactFetcher = fetch.New( - fetch.WithRetries(r.HttpRetry), - fetch.WithMaxDownloadSize(tar.UnlimitedUntarSize), - fetch.WithUntar(tar.WithMaxUntarSize(tar.UnlimitedUntarSize)), - fetch.WithLogger(nil), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&sourcev1.GitRepository{}, builder.WithPredicates(GitRepositoryRevisionChangePredicate{})). - Complete(r) -} - -// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories,verbs=get;list;watch -// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=gitrepositories/status,verbs=get - -func (r *GitRepositoryWatcher) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := ucplog.FromContextOrDiscard(ctx).WithValues("kind", "GitRepositoryWatcher", "name", req.Name, "namespace", req.Namespace) - ctx = logr.NewContext(ctx, logger) - - // Get the GitRepository object from the cluster - var repository sourcev1.GitRepository - if err := r.Get(ctx, req.NamespacedName, &repository); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - // Check if the Artifact field is set - artifact := repository.Status.Artifact - if artifact == nil { - logger.Info("No artifact found for GitRepository", "name", repository.Name) - return ctrl.Result{}, nil - } - - logger.Info("New revision detected", "revision", artifact.Revision) - - // Create temp dir to store the fetched artifact - tmpDir, err := os.MkdirTemp("", repository.Name) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to create temp dir, error: %w", err) - } - - defer func(path string) { - err := os.RemoveAll(path) - if err != nil { - logger.Error(err, "unable to remove temp dir") - } - }(tmpDir) - - // Fetch the artifact from the Source Controller - logger.Info("fetching artifact...", "url", artifact.URL) - if err := r.artifactFetcher.Fetch(artifact.URL, artifact.Digest, tmpDir); err != nil { - logger.Error(err, "unable to fetch artifact") - return ctrl.Result{}, err - } - - logger.Info("fetched artifact", "url", artifact.URL) - - files, err := os.ReadDir(tmpDir) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to list files, error: %w", err) - } - - // TODOWILLSMITH: Where to get ProviderConfig def? - var config sdkclients.ProviderConfig - - config.Radius = &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - } - config.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - } - - providerConfig, err := json.Marshal(config) - if err != nil { - logger.Error(err, "failed to run bicep build-params") - return ctrl.Result{}, err - } - - // Run bicep build on all root bicep files - for _, f := range files { - extension := path.Ext(f.Name()) - if extension == ".bicep" { - fileNameBase := strings.TrimSuffix(f.Name(), path.Ext(f.Name())) - deploymentTemplateName := repository.Name + "-" + fileNameBase - - template, err := r.runBicepBuild(ctx, tmpDir, f.Name()) - if err != nil { - logger.Error(err, "failed to run bicep build") - return ctrl.Result{}, err - } - - // Run bicep build-params on the bicepparams that matches the bicep file - // e.g. if the bicep file is main.bicep, the bicepparams file should be main.bicepparam - parameters := "{}" - parametersFileName := fileNameBase + ".bicepparam" - - // If the bicepparams file exists, run bicep build-params. Otherwise, use the - // default (empty) parameters. - if _, err := os.Stat(path.Join(tmpDir, parametersFileName)); err == nil { - parameters, err = r.runBicepBuildParams(ctx, tmpDir, parametersFileName) - if err != nil { - logger.Error(err, "failed to run bicep build-params") - return ctrl.Result{}, err - } - } - - // Now we should create (or update) each DeploymentTemplate for the bicep files - // specified in the git repository. - - // Create or update the deployment template. - logger.Info("Creating or updating Deployment Template", "name", deploymentTemplateName) - r.createOrUpdateDeploymentTemplate(ctx, deploymentTemplateName, template, parameters, string(providerConfig), repository.Name) - } - } - - // Get all DeploymentTemplates on the cluster that are associated with the git repository. - deploymentTemplates := &radappiov1alpha3.DeploymentTemplateList{} - err = r.Client.List(ctx, deploymentTemplates, client.MatchingFields{repositoryField: repository.Name}) - if err != nil { - logger.Error(err, "unable to list deployment templates") - return ctrl.Result{}, err - } - - // For all of the DeploymentTemplates on the cluster, check if the bicep file - // that it was created from still exists in the git repository. If it does not, - // delete the DeploymentTemplate. - for _, deploymentTemplate := range deploymentTemplates.Items { - deploymentTemplateFilename := fmt.Sprintf(strings.TrimPrefix(deploymentTemplate.Name, repository.Name+"-"), ".bicep") - if _, err := os.Stat(path.Join(tmpDir, deploymentTemplateFilename)); err != nil { - // File does not exist in the git repository, - // delete the DeploymentTemplate from the cluster - logger.Info("Deleting DeploymentTemplate", "name", deploymentTemplate.Name) - if err := r.Client.Delete(ctx, &deploymentTemplate); err != nil { - logger.Error(err, "unable to delete deployment template") - return ctrl.Result{}, err - } - - logger.Info("Deleted DeploymentTemplate", "name", deploymentTemplate.Name) - } - } - - return ctrl.Result{}, nil -} - -func repositoryIndexer(o client.Object) []string { - deploymentTemplate, ok := o.(*radappiov1alpha3.DeploymentTemplate) - if !ok { - return nil - } - return []string{deploymentTemplate.Spec.Repository} -} - -func (r *GitRepositoryWatcher) runBicepBuild(ctx context.Context, filepath, filename string) (armJSON string, err error) { - logger := ucplog.FromContextOrDiscard(ctx) - - logger.Info("Running bicep build on " + path.Join(filepath, filename)) - - outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicep", ".json")) - - cmd := exec.Command("bicep", "build", path.Join(filepath, filename), "--outfile", outfile) - cmd.Dir = filepath - - // Run the bicep build command - err = cmd.Run() - if err != nil { - logger.Error(err, "failed to run bicep build") - return "", err - } - - // Read the contents of the generated .json file - contents, err := os.ReadFile(outfile) - if err != nil { - logger.Error(err, "failed to read bicep build output") - return "", err - } - - return string(contents), nil -} - -func (r *GitRepositoryWatcher) runBicepBuildParams(ctx context.Context, filepath, filename string) (armJSON string, err error) { - logger := ucplog.FromContextOrDiscard(ctx) - - logger.Info("Running bicep build-params on " + filename) - - outfile := path.Join(filepath, strings.ReplaceAll(filename, ".bicepparam", ".bicepparam.json")) - - cmd := exec.Command("bicep", "build-params", path.Join(filepath, filename), "--outfile", outfile) - - // Run the bicep build-params command - err = cmd.Run() - if err != nil { - logger.Error(err, "failed to run bicep build") - return "", err - } - - // Read the contents of the generated .bicepparam.json file - contents, err := os.ReadFile(outfile) - if err != nil { - logger.Error(err, "failed to read bicep build-params output") - return "", err - } - - var params map[string]interface{} - err = json.Unmarshal(contents, ¶ms) - if err != nil { - logger.Error(err, "failed to unmarshal bicep build-params output") - return "", err - } - - if params["parameters"] == nil { - logger.Info("No parameters found in bicep build-params output") - return "{}", nil - } - - specifiedParams, err := json.Marshal(params["parameters"]) - if err != nil { - logger.Error(err, "failed to marshal parameters") - return "", err - } - - return string(specifiedParams), nil -} - -func (r *GitRepositoryWatcher) createOrUpdateDeploymentTemplate(ctx context.Context, fileName, template, parameters, providerConfig, repository string) { - logger := ucplog.FromContextOrDiscard(ctx) - - deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ - ObjectMeta: metav1.ObjectMeta{ - Name: fileName, - Namespace: RadiusSystemNamespace, - }, - Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: template, - Parameters: parameters, - ProviderConfig: providerConfig, - Repository: repository, - }, - } - - if err := r.Client.Get(ctx, client.ObjectKeyFromObject(deploymentTemplate), deploymentTemplate); err != nil { - if client.IgnoreNotFound(err) != nil { - logger.Error(err, "unable to get deployment template") - return - } - - if err := r.Client.Create(ctx, deploymentTemplate); err != nil { - logger.Error(err, "unable to create deployment template") - } - - logger.Info("Created Deployment Template", "name", deploymentTemplate.Name) - return - } - - if err := r.Client.Update(ctx, deploymentTemplate); err != nil { - logger.Error(err, "unable to create deployment template") - } - - logger.Info("Updated Deployment Template", "name", deploymentTemplate.Name) -} diff --git a/pkg/controller/reconciler/gitrepository_watcher_test.go b/pkg/controller/reconciler/gitrepository_watcher_test.go deleted file mode 100644 index f53e712213..0000000000 --- a/pkg/controller/reconciler/gitrepository_watcher_test.go +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright 2024 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconciler - -import "testing" - -func Test_GitRepositoryWatcher_Basic(t *testing.T) { - //TODOWILLSMITH: finish this test - t.Skip("TODO: finish this test") -} diff --git a/pkg/controller/service.go b/pkg/controller/service.go index d672a335f6..2aaf3c4a0e 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -33,8 +33,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - - sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var ( @@ -44,7 +42,6 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(radappiov1alpha3.AddToScheme(scheme)) - utilruntime.Must(sourcev1.AddToScheme(scheme)) } var _ hosting.Service = (*Service)(nil) @@ -128,13 +125,6 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentResource", err) } - err = (&reconciler.GitRepositoryWatcher{ - Client: mgr.GetClient(), - HttpRetry: reconciler.GitRepositoryHttpRetryCount, - }).SetupWithManager(mgr) - if err != nil { - return fmt.Errorf("failed to setup %s controller: %w", "GitRepositoryWatcher", err) - } if s.TLSCertDir == "" { logger.Info("Webhooks will be skipped. TLS certificates not present.") From 59ec01ddc5d5359d69a4a926967cbc5995364157 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 1 Nov 2024 12:42:45 -0700 Subject: [PATCH 12/65] FTs Signed-off-by: willdavsmith --- .../deploymenttemplate_reconciler.go | 4 +- .../deploymenttemplate_reconciler_test.go | 2 +- .../noncloud/deploymenttemplate_test.go | 175 ++++++++++++++++++ .../testdata/deploymenttemplate.bicep | 15 ++ test/radcli/cli.go | 27 +++ 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index f0c5e44e35..1d40264947 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -125,7 +125,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - scope, err := parseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to parse deployment scope: %w", err) } @@ -474,7 +474,7 @@ func (r *DeploymentTemplateReconciler) requeueDelay() time.Duration { return delay } -func parseDeploymentScopeFromProviderConfig(providerConfig string) (string, error) { +func ParseDeploymentScopeFromProviderConfig(providerConfig string) (string, error) { config := sdkclients.ProviderConfig{} json.Unmarshal([]byte(providerConfig), &config) diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index a9b4bbbb46..3b3db3fbf0 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -104,7 +104,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { // Deployment will be waiting for template to complete provisioning. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - scope, err := parseDeploymentScopeFromProviderConfig(status.ProviderConfig) + scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) require.NoError(t, err) require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go new file mode 100644 index 0000000000..e01d473079 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2023 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubernetes_test + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "testing" + "time" + + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "github.com/radius-project/radius/pkg/cli/clients_new/generated" + radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/pkg/controller/reconciler" + "github.com/radius-project/radius/pkg/sdk" + "github.com/radius-project/radius/test/rp" + "github.com/radius-project/radius/test/testcontext" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + watchtools "k8s.io/client-go/tools/watch" + controller_runtime "sigs.k8s.io/controller-runtime/pkg/client" +) + +func Test_DeploymentTemplate_K8sManifest(t *testing.T) { + ctx := testcontext.New(t) + opts := rp.NewRPTestOptions(t) + + name := "dt" + namespace := "kubernetes-deploymenttemplate-test" + + template, err := afero.ReadFile(afero.NewOsFs(), "testdata/deploymenttemplate.bicep") + require.NoError(t, err) + + paramsList := []string{ + fmt.Sprintf("name=%s", name), + fmt.Sprintf("namespace=%s", namespace), + } + + parametersMap := make(map[string]any, len(paramsList)) + for _, param := range paramsList { + parts := strings.SplitN(param, "=", 2) + parametersMap[parts[0]] = parts[1] + } + + parameters, err := json.Marshal(parametersMap) + require.NoError(t, err) + + // Create the namespace, if it already exists we can ignore the error. + _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) + + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), string(parameters), "{}", "") + + t.Run("Deploy", func(t *testing.T) { + t.Log("Creating DeploymentTemplate") + err = opts.Client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + }) + + t.Run("Check status", func(t *testing.T) { + ctx, cancel := testcontext.NewWithCancel(t) + defer cancel() + + // Get resource version + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + require.NoError(t, err) + + t.Log("Waiting for DeploymentTemplate ready") + deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) + require.NoError(t, err) + + // Doing a basic check that the deploymentTemplate has a resource provisioned. + require.NotEmpty(t, deploymentTemplate.Status.Resource) + + scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + require.NoError(t, err) + + client, err := generated.NewGenericResourcesClient(scope, "Applications.Core/environments", &aztoken.AnonymousCredential{}, sdk.NewClientOptions(opts.Connection)) + require.NoError(t, err) + + _, err = client.Get(ctx, deploymentTemplate.Name, nil) + require.NoError(t, err) + }) + + t.Run("Delete", func(t *testing.T) { + t.Log("Deleting DeploymentTemplate") + err = opts.Client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) + + require.Eventually(t, func() bool { + err = opts.Client.Get(ctx, types.NamespacedName{Name: "db", Namespace: namespace}, deploymentTemplate) + return apierrors.IsNotFound(err) + }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }) +} + +func makeDeploymentTemplate(name types.NamespacedName, template, parameters, providerConfig, repository string) *radappiov1alpha3.DeploymentTemplate { + deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + Repository: repository, + }, + } + + return deploymentTemplate +} + +func waitForDeploymentTemplateReady(t *testing.T, ctx context.Context, name types.NamespacedName, client controller_runtime.WithWatch, initialVersion string) (*radappiov1alpha3.DeploymentTemplate, error) { + // Based on https://gist.github.com/PrasadG193/52faed6499d2ec739f9630b9d044ffdc + lister := &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + listOptions := &controller_runtime.ListOptions{Raw: &options, Namespace: name.Namespace, FieldSelector: fields.ParseSelectorOrDie("metadata.name=" + name.Name)} + deploymentTemplates := &radappiov1alpha3.DeploymentTemplateList{} + err := client.List(ctx, deploymentTemplates, listOptions) + if err != nil { + return nil, err + } + + return deploymentTemplates, nil + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + listOptions := &controller_runtime.ListOptions{Raw: &options, Namespace: name.Namespace, FieldSelector: fields.ParseSelectorOrDie("metadata.name=" + name.Name)} + deploymentTemplates := &radappiov1alpha3.DeploymentTemplateList{} + return client.Watch(ctx, deploymentTemplates, listOptions) + }, + } + watcher, err := watchtools.NewRetryWatcher(initialVersion, lister) + require.NoError(t, err) + defer watcher.Stop() + + for { + event := <-watcher.ResultChan() + r, ok := event.Object.(*radappiov1alpha3.DeploymentTemplate) + if !ok { + // Not a deploymentTemplate, likely an event. + t.Logf("Received event: %+v", event) + continue + } + + t.Logf("Received deploymentTemplate. Status: %+v", r.Status) + if r.Status.Phrase == radappiov1alpha3.DeploymentTemplatePhraseReady { + return r, nil + } + } +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep b/test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep new file mode 100644 index 0000000000..8f2aa8df73 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep @@ -0,0 +1,15 @@ +extension radius + +param name string +param namespace string + +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: name + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: namespace + } + } +} diff --git a/test/radcli/cli.go b/test/radcli/cli.go index 0b548607cb..67e5cd6a59 100644 --- a/test/radcli/cli.go +++ b/test/radcli/cli.go @@ -287,6 +287,33 @@ func (cli *CLI) BicepPublish(ctx context.Context, file, target string) (string, return cli.RunCommand(ctx, args) } +func (cli *CLI) BicepGenerateKubernetesManifest(ctx context.Context, templateFilePath, outfile, environment string, parameters ...string) error { + // Check if the template file path exists + if _, err := os.Stat(templateFilePath); err != nil { + return fmt.Errorf("could not find template file: %s - %w", templateFilePath, err) + } + + args := []string{ + "bicep", + "generate-kubernetes-manifest", + templateFilePath, + } + + if environment != "" { + args = append(args, "--environment", environment) + } + + if outfile != "" { + args = append(args, "--outfile", outfile) + } + + for _, parameter := range parameters { + args = append(args, "--parameters", parameter) + } + _, err := cli.RunCommand(ctx, args) + return err +} + // Version runs the version command and returns the output as a string, or an error if the command fails. func (cli *CLI) Version(ctx context.Context) (string, error) { args := []string{ From 22a28e038b63ab47b1958d98c3517cc365062e5e Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 1 Nov 2024 13:01:52 -0700 Subject: [PATCH 13/65] FTs Signed-off-by: willdavsmith --- build/build.mk | 2 ++ .../generatekubernetesmanifest.go | 1 - pkg/controller/reconciler/const.go | 3 --- .../reconciler/deploymentresource_reconciler.go | 8 ++++++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build/build.mk b/build/build.mk index 7f4016cfdb..32189a52fc 100644 --- a/build/build.mk +++ b/build/build.mk @@ -153,3 +153,5 @@ clean: ## Cleans output directory. .PHONY: lint lint: ## Runs golangci-lint $(GOLANGCI_LINT) run --fix --timeout 5m + + \ No newline at end of file diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 340c156226..9aa44ea372 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -77,7 +77,6 @@ rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam -- commonflags.AddWorkspaceFlag(cmd) commonflags.AddResourceGroupFlag(cmd) commonflags.AddEnvironmentNameFlag(cmd) - commonflags.AddApplicationNameFlag(cmd) commonflags.AddParameterFlag(cmd) cmd.Flags().String("outfile", "", "Path of the generated DeploymentTemplate yaml file.") diff --git a/pkg/controller/reconciler/const.go b/pkg/controller/reconciler/const.go index bc82f4534f..2d1da9f6f3 100644 --- a/pkg/controller/reconciler/const.go +++ b/pkg/controller/reconciler/const.go @@ -56,7 +56,4 @@ const ( // RadiusSystemNamespace is the name of the system namespace where Radius resources are stored. RadiusSystemNamespace = "radius-system" - - // GitRepositoryHttpRetryCount is the number of times to retry GitRepository HTTP requests. - GitRepositoryHttpRetryCount = 9 ) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 3603839d79..6332887542 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -219,9 +219,13 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // fully processed any status changes until the async operation completes. deploymentResource.Status.ObservedGeneration = deploymentResource.Generation - // Check other resources that depend on this resource. + // NOTE: The following is a workaround for Radius API behavior. Since deleting + // an application or environment can leave hanging resources, we need to make sure to + // delete these resources before deleting the application or environment. + // Check other resources that depend on this resource. // List all DeploymentResource objects in the same namespace + // that have the same repository. deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) if err != nil { @@ -240,7 +244,7 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { envsCount++ } else if dr.Spec.Id != "" { - logger.Info(fmt.Sprintf("Other: %s", dr.Spec.Id)) + logger.Info("Resource is being used by another resource.", "resourceId", dr.Spec.Id) otherCount++ } } From 066c38c5c35e7e0b75e7d55e8525ef2092b98e14 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 1 Nov 2024 13:07:07 -0700 Subject: [PATCH 14/65] FTs Signed-off-by: willdavsmith --- build/build.mk | 2 -- 1 file changed, 2 deletions(-) diff --git a/build/build.mk b/build/build.mk index 32189a52fc..7f4016cfdb 100644 --- a/build/build.mk +++ b/build/build.mk @@ -153,5 +153,3 @@ clean: ## Cleans output directory. .PHONY: lint lint: ## Runs golangci-lint $(GOLANGCI_LINT) run --fix --timeout 5m - - \ No newline at end of file From d46d47940f2e515f67a53bb72c3db70d87382d04 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 1 Nov 2024 14:21:43 -0700 Subject: [PATCH 15/65] PR Signed-off-by: willdavsmith --- .../reconciler/deploymentresource_reconciler_test.go | 6 +++--- pkg/controller/reconciler/deploymenttemplate_reconciler.go | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index 2868f5ff29..a8499d8c3a 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -37,9 +37,9 @@ const ( DeploymentResourceTestWaitInterval = time.Second * 1 DeploymentResourceTestControllerDelayInterval = time.Millisecond * 100 - TestDeploymentResourceNamespace = "DeploymentResource-basic" - TestDeploymentResourceName = "test-DeploymentResource" - TestDeploymentResourceRadiusResourceGroup = "default-DeploymentResource-basic" + TestDeploymentResourceNamespace = "deploymentresource-basic" + TestDeploymentResourceName = "test-deploymentresource" + TestDeploymentResourceRadiusResourceGroup = "default-deploymentresource-basic" ) var ( diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 1d40264947..90313876c2 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -476,7 +476,10 @@ func (r *DeploymentTemplateReconciler) requeueDelay() time.Duration { func ParseDeploymentScopeFromProviderConfig(providerConfig string) (string, error) { config := sdkclients.ProviderConfig{} - json.Unmarshal([]byte(providerConfig), &config) + err := json.Unmarshal([]byte(providerConfig), &config) + if err != nil { + return "", fmt.Errorf("failed to unmarshal providerConfig: %w", err) + } if config.Deployments == nil { return "", fmt.Errorf("providerConfig.Deployments is nil") From 97676aa8b053c53b8188aa7ee639c57ecf4e1450 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 4 Nov 2024 14:18:55 -0800 Subject: [PATCH 16/65] fix Signed-off-by: willdavsmith --- .../generatekubernetesmanifest/generatekubernetesmanifest.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 9aa44ea372..cf5f9b4c44 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -335,6 +335,7 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string "template": string(marshalledTemplate), "parameters": string(marshalledParameters), "providerConfig": string(marshalledProviderConfig), + "repository": fileName, }, } From b0a89668dee399f4d1122455e2d861a79b17d2ee Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 4 Nov 2024 15:58:04 -0800 Subject: [PATCH 17/65] resource group Signed-off-by: willdavsmith --- deploy/install.sh | 2 +- .../reconciler/deploymenttemplate_reconciler.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/deploy/install.sh b/deploy/install.sh index 08d00285d7..a1866f66b4 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -125,7 +125,7 @@ downloadFile() { exit 1 fi - DOWNLOAD_URL="ghcr.io/radius-project/rad/${OS}-${ARCH}:latest" + DOWNLOAD_URL="ghcr.io/willdavsmith/mbcp/rad/${OS}-${ARCH}:latest" echo "Downloading edge CLI from ${DOWNLOAD_URL}..." oras pull $DOWNLOAD_URL -o $RADIUS_TMP_ROOT diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 90313876c2..effb182804 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -439,6 +439,14 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con return nil, fmt.Errorf("providerConfig.Deployments.Value.Scope is empty") } + // NOTE: using resource groups with lowercase here is a workaround for a casing bug in `rad app graph`. + // When https://github.com/radius-project/radius/issues/6422 is fixed we can use the more correct casing. + resourceGroupID := "/planes/radius/local/resourcegroups/default" + err = createResourceGroupIfNotExists(ctx, r.Radius, resourceGroupID) + if err != nil { + return nil, fmt.Errorf("failed to create resource group: %w", err) + } + logger.Info("Starting PUT operation.") properties := map[string]any{ "mode": "Incremental", From 3af3941d8e23ba03b93628c100e37119a50cf053 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 5 Nov 2024 17:05:04 -0800 Subject: [PATCH 18/65] CLI Signed-off-by: willdavsmith --- deploy/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/install.sh b/deploy/install.sh index a1866f66b4..bd3a074fb1 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -125,7 +125,7 @@ downloadFile() { exit 1 fi - DOWNLOAD_URL="ghcr.io/willdavsmith/mbcp/rad/${OS}-${ARCH}:latest" + DOWNLOAD_URL="ghcr.io/willdavsmith/flux-demo/rad/${OS}-${ARCH}:latest" echo "Downloading edge CLI from ${DOWNLOAD_URL}..." oras pull $DOWNLOAD_URL -o $RADIUS_TMP_ROOT From 5d52e6987414a9c933d107c8cb207ef598b15e4f Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Wed, 13 Nov 2024 23:42:55 -0800 Subject: [PATCH 19/65] Suggestions for users Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 14 +- .../radius/radapp.io_deploymenttemplates.yaml | 20 +- .../generatekubernetesmanifest.go | 238 +++++------------- .../v1alpha3/deploymentresource_types.go | 10 +- .../v1alpha3/deploymenttemplate_types.go | 12 +- .../v1alpha3/zz_generated.deepcopy.go | 9 +- .../deploymentresource_reconciler.go | 12 +- .../deploymenttemplate_reconciler.go | 46 ++-- pkg/controller/reconciler/util.go | 10 + 9 files changed, 148 insertions(+), 223 deletions(-) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 34346afde4..85e36997dd 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -54,9 +54,10 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - repository: - description: Repository is the name of the GitRepository that contains - the Bicep file. + rootFileName: + description: |- + RootFileName is the name of the Bicep file in the repository that + `bicep build` is run on. type: string type: object status: @@ -94,9 +95,10 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - repository: - description: Repository is the name of the GitRepository that contains - the Bicep file. + rootFileName: + description: |- + RootFileName is the name of the Bicep file in the repository that + `bicep build` is run on. type: string type: object type: object diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index dcd3d65c04..d38657b19e 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -53,14 +53,17 @@ spec: description: DeploymentTemplateSpec defines the desired state of DeploymentTemplate properties: parameters: + additionalProperties: + type: string description: Parameters is the ARM JSON parameters for the template. - type: string + type: object providerConfig: description: ProviderConfig specifies the scope for resources type: string - repository: - description: Repository is the name of the GitRepository that contains - the Bicep file. + rootFileName: + description: |- + RootFileName is the name of the Bicep file in the repository that + `bicep build` is run on. type: string template: description: Template is the ARM JSON manifest that defines the resources @@ -108,13 +111,14 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - repository: - description: Repository is the name of the GitRepository that contains - the Bicep file. - type: string resource: description: Resource is the resource id of the deployment. type: string + rootFileName: + description: |- + RootFileName is the name of the Bicep file in the repository that + `bicep build` is run on. + type: string template: description: Template is the ARM JSON manifest that defines the resources to deploy. diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index cf5f9b4c44..dbb0ccdfb1 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -21,24 +21,18 @@ import ( "encoding/json" "fmt" "path/filepath" - "sort" "strings" "github.com/spf13/afero" - "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/bicep" - "github.com/radius-project/radius/pkg/cli/clients" - "github.com/radius-project/radius/pkg/cli/clierrors" "github.com/radius-project/radius/pkg/cli/cmd/commonflags" "github.com/radius-project/radius/pkg/cli/connections" "github.com/radius-project/radius/pkg/cli/deploy" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" - "github.com/radius-project/radius/pkg/cli/workspaces" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/spf13/cobra" - "golang.org/x/exp/maps" "gopkg.in/yaml.v2" ) @@ -82,6 +76,10 @@ rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam -- cmd.Flags().String("outfile", "", "Path of the generated DeploymentTemplate yaml file.") _ = cmd.MarkFlagFilename("outfile", ".yaml") + cmd.Flags().String("azure-scope", "", "Scope for Azure deployment.") + cmd.Flags().String("aws-scope", "", "Scope for AWS deployment.") + cmd.Flags().String("deployment-scope", "", "Scope for the Radius deployment.") + return cmd, runner } @@ -93,13 +91,13 @@ type Runner struct { Deploy deploy.Interface Output output.Interface - FileSystem afero.Fs - EnvironmentNameOrID string - FilePath string - Parameters map[string]map[string]any - Workspace *workspaces.Workspace - Providers *clients.Providers - OutFile string + FileSystem afero.Fs + FilePath string + Parameters map[string]map[string]any + OutFile string + DeploymentScope string + AzureScope string + AWSScope string } // NewRunner creates a new instance of the `rad deploy` runner. @@ -115,73 +113,32 @@ func NewRunner(factory framework.Factory) *Runner { // Validate validates the inputs of the rad bicep generate-kubernetes-manifest command. func (r *Runner) Validate(cmd *cobra.Command, args []string) error { - workspace, err := cli.RequireWorkspace(cmd, r.ConfigHolder.Config, r.ConfigHolder.DirectoryConfig) + r.FilePath = args[0] + + var err error + r.DeploymentScope, err = cmd.Flags().GetString("deployment-scope") if err != nil { return err } - r.Workspace = workspace + if r.DeploymentScope == "" { + r.DeploymentScope = "/planes/radius/local/resourceGroups/default" + } - // Allow --group to override the scope - scope, err := cli.RequireScope(cmd, *workspace) + r.AzureScope, err = cmd.Flags().GetString("azure-scope") if err != nil { return err } - // We don't need to explicitly validate the existence of the scope, because we'll validate the existence - // of the environment later. That will give an appropriate error message for the case where the group - // does not exist. - workspace.Scope = scope - - r.EnvironmentNameOrID, err = cli.RequireEnvironmentNameOrID(cmd, args, *workspace) + r.AWSScope, err = cmd.Flags().GetString("aws-scope") if err != nil { return err } - // Validate that the environment exists. - // Right now we assume that every deployment uses a Radius Environment. - client, err := r.ConnectionFactory.CreateApplicationsManagementClient(cmd.Context(), *r.Workspace) + r.OutFile, err = cmd.Flags().GetString("outfile") if err != nil { return err } - env, err := client.GetEnvironment(cmd.Context(), r.EnvironmentNameOrID) - if err != nil { - // If the error is not a 404, return it - if !clients.Is404Error(err) { - return err - } - - // If the environment doesn't exist, but the user specified its name or resource id as - // a command-line option, return an error - if cli.DidSpecifyEnvironmentName(cmd, args) { - return clierrors.Message("The environment %q does not exist in scope %q. Run `rad env create` first. You could also provide the environment ID if the environment exists in a different group.", r.EnvironmentNameOrID, r.Workspace.Scope) - } - - // If we got here, it means that the error was a 404 and the user did not specify the environment name. - // This is fine, because an environment is not required. - } - - r.Providers = &clients.Providers{} - r.Providers.Radius = &clients.RadiusProvider{} - if env.ID != nil { - r.Providers.Radius.EnvironmentID = *env.ID - r.Workspace.Environment = r.Providers.Radius.EnvironmentID - } - - if env.Properties != nil && env.Properties.Providers != nil { - if env.Properties.Providers.Aws != nil { - r.Providers.AWS = &clients.AWSProvider{ - Scope: *env.Properties.Providers.Aws.Scope, - } - } - if env.Properties.Providers.Azure != nil { - r.Providers.Azure = &clients.AzureProvider{ - Scope: *env.Properties.Providers.Azure.Scope, - } - } - } - - r.FilePath = args[0] parameterArgs, err := cmd.Flags().GetStringArray("parameters") if err != nil { @@ -208,27 +165,13 @@ func (r *Runner) Run(ctx context.Context) error { return err } - // This is the earliest point where we can inject parameters, we have - // to wait until the template is prepared. - err = r.injectAutomaticParameters(template) - if err != nil { - return err - } - - // This is the earliest point where we can report missing parameters, we have - // to wait until the template is prepared. - err = r.reportMissingParameters(template) - if err != nil { - return err - } - // create a DeploymentTemplate yaml file // with the basefilename from the bicepfile if r.OutFile == "" { r.OutFile = strings.TrimSuffix(filepath.Base(r.FilePath), filepath.Ext(r.FilePath)) + ".yaml" } - deploymentTemplate, err := r.generateDeploymentTemplate(r.OutFile, template, r.Parameters, r.Providers) + deploymentTemplate, err := r.generateDeploymentTemplate(filepath.Base(r.FilePath), template, r.Parameters) if err != nil { return err } @@ -244,84 +187,23 @@ func (r *Runner) Run(ctx context.Context) error { return nil } -func (r *Runner) injectAutomaticParameters(template map[string]any) error { - if r.Providers.Radius.EnvironmentID != "" { - err := bicep.InjectEnvironmentParam(template, r.Parameters, r.Providers.Radius.EnvironmentID) - if err != nil { - return err - } - } - - return nil -} - -func (r *Runner) reportMissingParameters(template map[string]any) error { - declaredParameters, err := bicep.ExtractParameters(template) - if err != nil { - return err - } - - errors := map[string]string{} - for parameter := range declaredParameters { - // Case-invariant lookup on the user-provided values - match := false - for provided := range r.Parameters { - if strings.EqualFold(parameter, provided) { - match = true - break - } - } - - if match { - // Has user-provided value - continue - } - - if _, ok := bicep.DefaultValue(declaredParameters[parameter]); ok { - // Has default value - continue - } - - // Special case the parameters that are automatically injected - if strings.EqualFold(parameter, "environment") { - errors[parameter] = "The template requires an environment. Use --environment to specify the environment name." - } else { - errors[parameter] = fmt.Sprintf("The template requires a parameter %q. Use --parameters %s= to specify the value.", parameter, parameter) - } - } - - if len(errors) == 0 { - return nil - } - - keys := maps.Keys(errors) - sort.Strings(keys) - - details := []string{} - for _, key := range keys { - details = append(details, fmt.Sprintf(" - %v", errors[key])) - } - - return clierrors.Message("The template %q could not be deployed because of the following errors:\n\n%v", r.FilePath, strings.Join(details, "\n")) -} - // generateDeploymentTemplate generates a DeploymentTemplate Custom Resource from the given template and parameters. -func (r *Runner) generateDeploymentTemplate(fileName string, template map[string]any, parameters map[string]map[string]any, providers *clients.Providers) (map[string]any, error) { - marshalledTemplate, err := json.Marshal(template) +func (r *Runner) generateDeploymentTemplate(fileName string, template map[string]any, parameters map[string]map[string]any) (map[string]any, error) { + marshalledTemplate, err := json.MarshalIndent(template, "", " ") if err != nil { return nil, err } - marshalledParameters, err := json.Marshal(parameters) + providerConfig := r.generateProviderConfig() + + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") if err != nil { return nil, err } - providerConfig := r.convertProvidersToProviderConfig(providers) - - marshalledProviderConfig, err := json.Marshal(providerConfig) - if err != nil { - return nil, err + params := make(map[string]string) + for k, v := range parameters { + params[k] = v["value"].(string) } deploymentTemplate := map[string]any{ @@ -333,9 +215,9 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string }, "spec": map[string]any{ "template": string(marshalledTemplate), - "parameters": string(marshalledParameters), + "parameters": params, "providerConfig": string(marshalledProviderConfig), - "repository": fileName, + "rootFileName": fileName, }, } @@ -366,38 +248,36 @@ func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string] } // convertProvidersToProviderConfig converts the the clients.Providers to sdkclients.ProviderConfig. -func (r *Runner) convertProvidersToProviderConfig(providers *clients.Providers) (providerConfig sdkclients.ProviderConfig) { +func (r *Runner) generateProviderConfig() (providerConfig sdkclients.ProviderConfig) { providerConfig = sdkclients.ProviderConfig{} - if providers != nil { - if providers.AWS != nil { - providerConfig.AWS = &sdkclients.AWS{ - Type: "aws", - Value: sdkclients.Value{ - Scope: providers.AWS.Scope, - }, - } + if r.AWSScope != "" { + providerConfig.AWS = &sdkclients.AWS{ + Type: "aws", + Value: sdkclients.Value{ + Scope: r.AWSScope, + }, } - if providers.Azure != nil { - providerConfig.Az = &sdkclients.Az{ - Type: "azure", - Value: sdkclients.Value{ - Scope: providers.Azure.Scope, - }, - } + } + if r.AzureScope != "" { + providerConfig.Az = &sdkclients.Az{ + Type: "azure", + Value: sdkclients.Value{ + Scope: r.AzureScope, + }, + } + } + if r.DeploymentScope != "" { + providerConfig.Radius = &sdkclients.Radius{ + Type: "radius", + Value: sdkclients.Value{ + Scope: r.DeploymentScope, + }, } - if providers.Radius != nil { - providerConfig.Radius = &sdkclients.Radius{ - Type: "radius", - Value: sdkclients.Value{ - Scope: r.Workspace.Scope, - }, - } - providerConfig.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: r.Workspace.Scope, - }, - } + providerConfig.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: r.DeploymentScope, + }, } } diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 1ae4b62b1c..50b7a87ff8 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -28,8 +28,9 @@ type DeploymentResourceSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // Repository is the name of the GitRepository that contains the Bicep file. - Repository string `json:"repository,omitempty"` + // RootFileName is the name of the Bicep file in the repository that + // `bicep build` is run on. + RootFileName string `json:"rootFileName,omitempty"` } // DeploymentResourceStatus defines the observed state of DeploymentResource @@ -40,8 +41,9 @@ type DeploymentResourceStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // Repository is the name of the GitRepository that contains the Bicep file. - Repository string `json:"repository,omitempty"` + // RootFileName is the name of the Bicep file in the repository that + // `bicep build` is run on. + RootFileName string `json:"rootFileName,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index ecbc7355ae..183ac18599 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -26,13 +26,14 @@ type DeploymentTemplateSpec struct { Template string `json:"template,omitempty"` // Parameters is the ARM JSON parameters for the template. - Parameters string `json:"parameters,omitempty"` + Parameters map[string]string `json:"parameters,omitempty"` // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // Repository is the name of the GitRepository that contains the Bicep file. - Repository string `json:"repository,omitempty"` + // RootFileName is the name of the Bicep file in the repository that + // `bicep build` is run on. + RootFileName string `json:"rootFileName,omitempty"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -49,8 +50,9 @@ type DeploymentTemplateStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // Repository is the name of the GitRepository that contains the Bicep file. - Repository string `json:"repository,omitempty"` + // RootFileName is the name of the Bicep file in the repository that + // `bicep build` is run on. + RootFileName string `json:"rootFileName,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` diff --git a/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go b/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go index aa25b10470..986e7ef2d0 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go +++ b/pkg/controller/api/radapp.io/v1alpha3/zz_generated.deepcopy.go @@ -123,7 +123,7 @@ func (in *DeploymentTemplate) DeepCopyInto(out *DeploymentTemplate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -180,6 +180,13 @@ func (in *DeploymentTemplateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentTemplateSpec) DeepCopyInto(out *DeploymentTemplateSpec) { *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentTemplateSpec. diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 6332887542..89387a3477 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -40,7 +40,7 @@ import ( ) const ( - repositoryField = "spec.repository" + rootFileNameField = "spec.rootFileName" ) // DeploymentResourceReconciler reconciles a DeploymentResource object. @@ -225,9 +225,9 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // Check other resources that depend on this resource. // List all DeploymentResource objects in the same namespace - // that have the same repository. + // that have the same rootFileName. deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{repositoryField: deploymentResource.Spec.Repository}) + err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{deploymentResource.Spec.RootFileName: deploymentResource.Spec.RootFileName}) if err != nil { return ctrl.Result{}, nil } @@ -348,17 +348,17 @@ func (r *DeploymentResourceReconciler) requeueDelay() time.Duration { return delay } -func deploymentResourceRepositoryIndexer(o client.Object) []string { +func deploymentResourceRootFileNameIndexer(o client.Object) []string { deploymentResource, ok := o.(*radappiov1alpha3.DeploymentResource) if !ok { return nil } - return []string{deploymentResource.Spec.Repository} + return []string{deploymentResource.Spec.RootFileName} } // SetupWithManager sets up the controller with the Manager. func (r *DeploymentResourceReconciler) SetupWithManager(mgr ctrl.Manager) error { - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentResource{}, repositoryField, deploymentResourceRepositoryIndexer); err != nil { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentResource{}, rootFileNameField, deploymentResourceRootFileNameIndexer); err != nil { return err } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index effb182804..5573cfd2ac 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -199,7 +199,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d Spec: radappiov1alpha3.DeploymentResourceSpec{ Id: outputResourceId, ProviderConfig: deploymentTemplate.Spec.ProviderConfig, - Repository: deploymentTemplate.Spec.Repository, + RootFileName: deploymentTemplate.Spec.RootFileName, }, } @@ -235,6 +235,13 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d } } + parameters, err := json.Marshal(deploymentTemplate.Spec.Parameters) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to marshal parameters: %w", err) + } + + fmt.Println("PARAMETERS: ", string(parameters)) + providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { @@ -247,10 +254,10 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template - deploymentTemplate.Status.Parameters = deploymentTemplate.Spec.Parameters + deploymentTemplate.Status.Parameters = string(parameters) deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig - deploymentTemplate.Status.Repository = deploymentTemplate.Spec.Repository + deploymentTemplate.Status.RootFileName = deploymentTemplate.Spec.RootFileName return ctrl.Result{}, nil } @@ -404,10 +411,17 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) + parameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) + + stringifiedParameters, err := json.Marshal(parameters) + if err != nil { + return nil, fmt.Errorf("failed to marshal parameters: %w", err) + } + // If the resource is already created and is up-to-date, then we don't need to do anything. if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && - deploymentTemplate.Status.Parameters == deploymentTemplate.Spec.Parameters && - deploymentTemplate.Status.Repository == deploymentTemplate.Spec.Repository && + deploymentTemplate.Status.Parameters == string(stringifiedParameters) && + deploymentTemplate.Status.RootFileName == deploymentTemplate.Spec.RootFileName && deploymentTemplate.Status.ProviderConfig == deploymentTemplate.Spec.ProviderConfig { logger.Info("Resource is already created and is up-to-date.") return nil, nil @@ -416,17 +430,11 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con logger.Info("Template, parameters, repository, or providerConfig have changed, starting PUT operation.") var template any - err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) + err = json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) if err != nil { return nil, fmt.Errorf("failed to unmarshal template: %w", err) } - var parameters any - err = json.Unmarshal([]byte(deploymentTemplate.Spec.Parameters), ¶meters) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal parameters: %w", err) - } - providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { @@ -482,9 +490,19 @@ func (r *DeploymentTemplateReconciler) requeueDelay() time.Duration { return delay } -func ParseDeploymentScopeFromProviderConfig(providerConfig string) (string, error) { +func ParseDeploymentScopeFromProviderConfig(providerConfig any) (string, error) { + var data []byte + switch v := providerConfig.(type) { + case string: + data = []byte(v) + case []byte: + data = v + default: + return "", fmt.Errorf("providerConfig must be a string or []byte, got %T", providerConfig) + } + config := sdkclients.ProviderConfig{} - err := json.Unmarshal([]byte(providerConfig), &config) + err := json.Unmarshal([]byte(data), &config) if err != nil { return "", fmt.Errorf("failed to unmarshal providerConfig: %w", err) } diff --git a/pkg/controller/reconciler/util.go b/pkg/controller/reconciler/util.go index 9ff0aa63de..93bd8e0a92 100644 --- a/pkg/controller/reconciler/util.go +++ b/pkg/controller/reconciler/util.go @@ -286,3 +286,13 @@ func generateDeploymentResourceName(resourceId string) string { return resourceBaseName } + +func convertToARMJSONParameters(parameters map[string]string) map[string]map[string]string { + armJSONParameters := make(map[string]map[string]string, len(parameters)) + for key, value := range parameters { + armJSONParameters[key] = map[string]string{ + "value": value, + } + } + return armJSONParameters +} From b676311f8631121c564ec3161212af790fbc4ab2 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Wed, 20 Nov 2024 17:29:33 -0800 Subject: [PATCH 20/65] gkm unit tests Signed-off-by: willdavsmith --- .../generatekubernetesmanifest.go | 2 +- .../generatekubernetesmanifest_test.go | 663 ++++++++++++------ .../testdata/aws.yaml | 9 - .../testdata/aws/aws.bicep | 11 + .../testdata/aws/aws.yaml | 74 ++ .../testdata/azure.yaml | 9 - .../testdata/azure/azure.bicep | 9 + .../testdata/azure/azure.yaml | 59 ++ .../testdata/basic.yaml | 9 - .../testdata/basic/basic.bicep | 20 + .../testdata/basic/basic.yaml | 70 ++ .../testdata/module/module.bicep | 14 + .../testdata/module/module.yaml | 119 ++++ .../testdata/module/storage.bicep | 14 + .../testdata/parameters.json | 9 + .../testdata/parameters/parameters.bicep | 23 + .../testdata/parameters/parameters.json | 9 + .../testdata/parameters/parameters.yaml | 80 +++ .../testdata/value.yaml | 9 - 19 files changed, 963 insertions(+), 249 deletions(-) delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index dbb0ccdfb1..7a12b66d63 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -247,7 +247,7 @@ func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string] return nil } -// convertProvidersToProviderConfig converts the the clients.Providers to sdkclients.ProviderConfig. +// generateProviderConfig generates a ProviderConfig object based on the given scopes. func (r *Runner) generateProviderConfig() (providerConfig sdkclients.ProviderConfig) { providerConfig = sdkclients.ProviderConfig{} if r.AWSScope != "" { diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go index 244fd3f443..629bb4895d 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go @@ -18,21 +18,17 @@ package bicep import ( "context" - "fmt" + "encoding/json" "os" "path/filepath" "testing" "github.com/radius-project/radius/pkg/cli/bicep" - "github.com/radius-project/radius/pkg/cli/clients" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" - "github.com/radius-project/radius/pkg/cli/workspaces" "github.com/radius-project/radius/test/radcli" "github.com/spf13/afero" - "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" - "github.com/radius-project/radius/pkg/to" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) @@ -44,7 +40,6 @@ func Test_CommandValidation(t *testing.T) { func Test_Validate(t *testing.T) { configWithWorkspace := radcli.LoadConfigWithWorkspace(t) testcases := []radcli.ValidateInput{ - { Name: "rad bicep generate-kubernetes-manifest - valid", Input: []string{"app.bicep"}, @@ -53,11 +48,9 @@ func Test_Validate(t *testing.T) { ConfigFilePath: "", Config: configWithWorkspace, }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/Applications.Core/environments/test-environment"). - Return(v20231001preview.EnvironmentResource{}, nil). - Times(1) + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { @@ -68,127 +61,108 @@ func Test_Validate(t *testing.T) { ConfigFilePath: "", Config: configWithWorkspace, }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), radcli.TestEnvironmentID). - Return(v20231001preview.EnvironmentResource{}, nil). - Times(1) - + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with environment", - Input: []string{"app.bicep", "-e", "prod"}, + Name: "rad bicep generate-kubernetes-manifest - valid with parameters file", + Input: []string{"app.bicep", "--parameters", "@testdata/parameters.json"}, ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", Config: configWithWorkspace, }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "prod"). - Return(v20231001preview.EnvironmentResource{ - Properties: &v20231001preview.EnvironmentProperties{ - Providers: &v20231001preview.Providers{ - Azure: &v20231001preview.ProvidersAzure{ - Scope: to.Ptr("/subscriptions/test-subId/resourceGroups/test-rg"), - }, - }, - }, - }, nil). - Times(1) + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - env does not exist invalid", - Input: []string{"app.bicep", "-e", "prod"}, + Name: "rad bicep generate-kubernetes-manifest - invalid parameter format", + Input: []string{"app.bicep", "--parameters", "invalid-format"}, ExpectedValid: false, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", - Config: configWithWorkspace, + Config: radcli.LoadEmptyConfig(t), }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "prod"). - Return(v20231001preview.EnvironmentResource{}, radcli.Create404Error()). - Times(1) + }, + { + Name: "rad bicep generate-kubernetes-manifest - too many args", + Input: []string{"app.bicep", "anotherfile.bicep"}, + ExpectedValid: false, + ConfigHolder: framework.ConfigHolder{ + ConfigFilePath: "", + Config: radcli.LoadEmptyConfig(t), }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with env ID", - Input: []string{"app.bicep", "-e", "/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"}, + Name: "rad bicep generate-kubernetes-manifest - valid with outfile", + Input: []string{"app.bicep", "--outfile", "test.yaml"}, ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", Config: configWithWorkspace, }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"). - Return(v20231001preview.EnvironmentResource{ - ID: to.Ptr("/planes/radius/local/resourceGroups/test-resource-group/providers/applications.core/environments/prod"), - }, nil). - Times(1) - }, - ValidateCallback: func(t *testing.T, obj framework.Runner) { - runner := obj.(*Runner) - scope := "/planes/radius/local/resourceGroups/test-resource-group" - environmentID := scope + "/providers/applications.core/environments/prod" - require.Equal(t, scope, runner.Workspace.Scope) - require.Equal(t, environmentID, runner.Workspace.Environment) + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - fallback workspace", - Input: []string{"app.bicep", "--group", "my-group", "--environment", "prod"}, - ExpectedValid: true, + Name: "rad bicep generate-kubernetes-manifest - invalid outfile", + Input: []string{"app.bicep", "test.json"}, + ExpectedValid: false, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", Config: radcli.LoadEmptyConfig(t), }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "prod"). - Return(v20231001preview.EnvironmentResource{}, nil). - Times(1) - }, }, { - Name: "rad bicep generate-kubernetes-manifest - fallback workspace requires resource group", - Input: []string{"app.bicep", "--environment", "prod"}, - ExpectedValid: false, + Name: "rad bicep generate-kubernetes-manifest - valid with azure scope", + Input: []string{"app.bicep", "--azure-scope", "azure-scope-value"}, + ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), + Config: configWithWorkspace, + }, + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "azure-scope-value", runner.AzureScope) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - too many args", - Input: []string{"app.bicep", "anotherfile.json"}, - ExpectedValid: false, + Name: "rad bicep generate-kubernetes-manifest - valid with aws scope", + Input: []string{"app.bicep", "--aws-scope", "aws-scope-value"}, + ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), + Config: configWithWorkspace, + }, + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "aws-scope-value", runner.AWSScope) + require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with outfile", - Input: []string{"app.bicep", "--outfile", "test.yaml"}, + Name: "rad bicep generate-kubernetes-manifest - valid with deployment scope", + Input: []string{"app.bicep", "--deployment-scope", "deployment-scope-value"}, ExpectedValid: true, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", Config: configWithWorkspace, }, - ConfigureMocks: func(mocks radcli.ValidateMocks) { - mocks.ApplicationManagementClient.EXPECT(). - GetEnvironment(gomock.Any(), "/planes/radius/local/resourceGroups/test-resource-group/providers/Applications.Core/environments/test-environment"). - Return(v20231001preview.EnvironmentResource{}, nil). - Times(1) + ValidateCallback: func(t *testing.T, r framework.Runner) { + runner := r.(*Runner) + require.Equal(t, "deployment-scope-value", runner.DeploymentScope) }, }, { - Name: "rad bicep generate-kubernetes-manifest - invalid outfile", - Input: []string{"app.bicep", "anotherfile.json"}, + Name: "rad bicep generate-kubernetes-manifest - missing file argument", + Input: []string{}, ExpectedValid: false, ConfigHolder: framework.ConfigHolder{ ConfigFilePath: "", @@ -201,41 +175,79 @@ func Test_Validate(t *testing.T) { } func Test_Run(t *testing.T) { - t.Run("Create basic DeploymentTemplate", func(t *testing.T) { + t.Run("Create DeploymentTemplate (basic)", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + template := ` + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "10886769892319697000", + "version": "0.30.23.60470" + } + }, + "resources": { + "basic": { + "import": "Radius", + "properties": { + "name": "basic", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "default", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + } + } + } + ` + + var templateMap map[string]any + err := json.Unmarshal([]byte(template), &templateMap) + require.NoError(t, err) + bicep := bicep.NewMockInterface(ctrl) bicep.EXPECT(). PrepareTemplate("basic.bicep"). - Return(map[string]any{}, nil). + Return(templateMap, nil). Times(1) - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", - }, - Scope: "/planes/radius/local/resourceGroups/test-resource-group", - Name: "kind-kind", - } - provider := &clients.Providers{ - Radius: &clients.RadiusProvider{ - EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), - }, - } - filePath := "basic.bicep" outputSink := &output.MockOutput{} runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - Workspace: workspace, - Providers: provider, - FileSystem: afero.NewMemMapFs(), + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + FileSystem: afero.NewMemMapFs(), + DeploymentScope: "/planes/radius/local/resourceGroups/default", } fileExists, err := afero.Exists(runner.FileSystem, "basic.yaml") @@ -251,127 +263,272 @@ func Test_Run(t *testing.T) { require.Equal(t, "basic.yaml", runner.OutFile) - expected, err := os.ReadFile(filepath.Join("testdata", "basic.yaml")) + expected, err := os.ReadFile(filepath.Join("testdata", "basic", "basic.yaml")) require.NoError(t, err) - // assert that the file contents are as expected actual, err := afero.ReadFile(runner.FileSystem, "basic.yaml") require.NoError(t, err) require.Equal(t, string(expected), string(actual)) }) - t.Run("Create DeploymentTemplate with template content", func(t *testing.T) { + t.Run("Create DeploymentTemplate (parameters)", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + template := ` + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "289770176196104222", + "version": "0.30.23.60470" + } + }, + "parameters": { + "kubernetesNamespace": { + "defaultValue": "default", + "type": "string" + }, + "tag": { + "defaultValue": "latest", + "type": "string" + } + }, + "resources": { + "parameters": { + "import": "Radius", + "properties": { + "name": "parameters", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "[parameters('kubernetesNamespace')]", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('ghcr.io/radius-project/recipes/local-dev/rediscaches:{0}', parameters('tag'))]" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + } + } + } + ` + + var templateMap map[string]any + err := json.Unmarshal([]byte(template), &templateMap) + require.NoError(t, err) + bicep := bicep.NewMockInterface(ctrl) bicep.EXPECT(). - PrepareTemplate("value.bicep"). - Return(map[string]any{ - "resources": []map[string]any{ - { - "some-key": "some-value", - }, - }, - "parameters": map[string]any{ - "kubernetesNamespace": map[string]any{}, - }, - }, nil). + PrepareTemplate("parameters.bicep"). + Return(templateMap, nil). Times(1) - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", - }, - Scope: "/planes/radius/local/resourceGroups/test-resource-group", - Name: "kind-kind", - } - provider := &clients.Providers{ - Radius: &clients.RadiusProvider{ - EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), - }, - } + filePath := "parameters.bicep" - parameters := map[string]map[string]any{ - "kubernetesNamespace": { - "value": "test-namespace", - }, + outputSink := &output.MockOutput{} + runner := &Runner{ + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + FileSystem: afero.NewMemMapFs(), + DeploymentScope: "/planes/radius/local/resourceGroups/default", } - filePath := "value.bicep" + fileExists, err := afero.Exists(runner.FileSystem, "parameters.yaml") + require.NoError(t, err) + require.False(t, fileExists) + + err = runner.Run(context.Background()) + require.NoError(t, err) + + fileExists, err = afero.Exists(runner.FileSystem, "parameters.yaml") + require.NoError(t, err) + require.True(t, fileExists) + + require.Equal(t, "parameters.yaml", runner.OutFile) + + expected, err := os.ReadFile(filepath.Join("testdata", "parameters", "parameters.yaml")) + require.NoError(t, err) + + actual, err := afero.ReadFile(runner.FileSystem, "parameters.yaml") + require.NoError(t, err) + require.Equal(t, string(expected), string(actual)) + }) + + t.Run("Create DeploymentTemplate (aws)", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + template := ` + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "AWS": { + "provider": "AWS", + "version": "latest" + }, + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "4336724644513409792", + "version": "0.30.23.60470" + } + }, + "parameters": { + "bucketName": { + "defaultValue": "gkm-bucket", + "type": "string" + } + }, + "resources": { + "bucket": { + "import": "AWS", + "properties": { + "alias": "[parameters('bucketName')]", + "properties": { + "BucketName": "[parameters('bucketName')]" + } + }, + "type": "AWS.S3/Bucket@default" + } + } + } + ` + + var templateMap map[string]any + err := json.Unmarshal([]byte(template), &templateMap) + require.NoError(t, err) + + bicep := bicep.NewMockInterface(ctrl) + bicep.EXPECT(). + PrepareTemplate("aws.bicep"). + Return(templateMap, nil). + Times(1) + + filePath := "aws.bicep" outputSink := &output.MockOutput{} runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: parameters, - Workspace: workspace, - Providers: provider, - FileSystem: afero.NewMemMapFs(), + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + FileSystem: afero.NewMemMapFs(), + AWSScope: "awsscope", + DeploymentScope: "/planes/radius/local/resourceGroups/default", } - fileExists, err := afero.Exists(runner.FileSystem, "value.yaml") + fileExists, err := afero.Exists(runner.FileSystem, "aws.yaml") require.NoError(t, err) require.False(t, fileExists) err = runner.Run(context.Background()) require.NoError(t, err) - fileExists, err = afero.Exists(runner.FileSystem, "value.yaml") + fileExists, err = afero.Exists(runner.FileSystem, "aws.yaml") require.NoError(t, err) require.True(t, fileExists) - require.Equal(t, "value.yaml", runner.OutFile) + require.Equal(t, "aws.yaml", runner.OutFile) - expected, err := os.ReadFile(filepath.Join("testdata", "value.yaml")) + expected, err := os.ReadFile(filepath.Join("testdata", "aws", "aws.yaml")) require.NoError(t, err) - // assert that the file contents are as expected - actual, err := afero.ReadFile(runner.FileSystem, "value.yaml") + actual, err := afero.ReadFile(runner.FileSystem, "aws.yaml") require.NoError(t, err) require.Equal(t, string(expected), string(actual)) }) - t.Run("Create DeploymentTemplate with Azure scope", func(t *testing.T) { + t.Run("Create DeploymentTemplate (azure)", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + template := ` + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "14111843528652336728", + "version": "0.30.23.60470" + } + }, + "resources": { + "storageAccount": { + "apiVersion": "2021-04-01", + "kind": "StorageV2", + "location": "eastus", + "name": "gkmstorageaccount", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "type": "Microsoft.Storage/storageAccounts" + } + } + } + ` + + var templateMap map[string]any + err := json.Unmarshal([]byte(template), &templateMap) + require.NoError(t, err) + bicep := bicep.NewMockInterface(ctrl) bicep.EXPECT(). PrepareTemplate("azure.bicep"). - Return(map[string]any{}, nil). + Return(templateMap, nil). Times(1) - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", - }, - Scope: "/planes/radius/local/resourceGroups/test-resource-group", - Name: "kind-kind", - } - provider := &clients.Providers{ - Radius: &clients.RadiusProvider{ - EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), - }, - Azure: &clients.AzureProvider{ - Scope: "/subscriptions/test-subId/resourceGroups/test-rg", - }, - } - filePath := "azure.bicep" outputSink := &output.MockOutput{} runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - Workspace: workspace, - Providers: provider, - FileSystem: afero.NewMemMapFs(), + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + FileSystem: afero.NewMemMapFs(), + AzureScope: "azurescope", + DeploymentScope: "/planes/radius/local/resourceGroups/default", } fileExists, err := afero.Exists(runner.FileSystem, "azure.yaml") @@ -387,73 +544,155 @@ func Test_Run(t *testing.T) { require.Equal(t, "azure.yaml", runner.OutFile) - expected, err := os.ReadFile(filepath.Join("testdata", "azure.yaml")) + expected, err := os.ReadFile(filepath.Join("testdata", "azure", "azure.yaml")) require.NoError(t, err) - // assert that the file contents are as expected actual, err := afero.ReadFile(runner.FileSystem, "azure.yaml") require.NoError(t, err) require.Equal(t, string(expected), string(actual)) }) - t.Run("Create DeploymentTemplate with AWS scope", func(t *testing.T) { + t.Run("Create DeploymentTemplate (module)", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + template := ` + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "1040374933922883026", + "version": "0.30.23.60470" + } + }, + "outputs": { + "storageAccountId": { + "type": "string", + "value": "[reference('storageModule').outputs.storageAccountId.value]" + } + }, + "parameters": { + "location": { + "defaultValue": "[resourceGroup().location]", + "type": "string" + }, + "storageAccountName": { + "type": "string" + } + }, + "resources": { + "storageModule": { + "apiVersion": "2022-09-01", + "name": "storageModule", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "17553429517046312167", + "version": "0.30.23.60470" + } + }, + "outputs": { + "storageAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + } + }, + "resources": { + "storageAccount": { + "apiVersion": "2021-04-01", + "kind": "StorageV2", + "location": "[parameters('location')]", + "name": "[parameters('storageAccountName')]", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "type": "Microsoft.Storage/storageAccounts" + } + } + } + }, + "type": "Microsoft.Resources/deployments" + } + } + } + ` + + var templateMap map[string]any + err := json.Unmarshal([]byte(template), &templateMap) + require.NoError(t, err) + bicep := bicep.NewMockInterface(ctrl) bicep.EXPECT(). - PrepareTemplate("aws.bicep"). - Return(map[string]any{}, nil). + PrepareTemplate("module.bicep"). + Return(templateMap, nil). Times(1) - workspace := &workspaces.Workspace{ - Connection: map[string]any{ - "kind": "kubernetes", - "context": "kind-kind", - }, - Scope: "/planes/radius/local/resourceGroups/test-resource-group", - Name: "kind-kind", - } - provider := &clients.Providers{ - Radius: &clients.RadiusProvider{ - EnvironmentID: fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/applications.core/environments/%s", radcli.TestEnvironmentName, radcli.TestEnvironmentName), - }, - AWS: &clients.AWSProvider{ - Scope: "awsscope", - }, - } - - filePath := "aws.bicep" + filePath := "module.bicep" outputSink := &output.MockOutput{} runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - Workspace: workspace, - Providers: provider, - FileSystem: afero.NewMemMapFs(), + Bicep: bicep, + Output: outputSink, + FilePath: filePath, + Parameters: map[string]map[string]any{}, + FileSystem: afero.NewMemMapFs(), + DeploymentScope: "/planes/radius/local/resourceGroups/default", } - fileExists, err := afero.Exists(runner.FileSystem, "aws.yaml") + fileExists, err := afero.Exists(runner.FileSystem, "module.yaml") require.NoError(t, err) require.False(t, fileExists) err = runner.Run(context.Background()) require.NoError(t, err) - fileExists, err = afero.Exists(runner.FileSystem, "aws.yaml") + fileExists, err = afero.Exists(runner.FileSystem, "module.yaml") require.NoError(t, err) require.True(t, fileExists) - require.Equal(t, "aws.yaml", runner.OutFile) + require.Equal(t, "module.yaml", runner.OutFile) - expected, err := os.ReadFile(filepath.Join("testdata", "aws.yaml")) + expected, err := os.ReadFile(filepath.Join("testdata", "module", "module.yaml")) require.NoError(t, err) - // assert that the file contents are as expected - actual, err := afero.ReadFile(runner.FileSystem, "aws.yaml") + actual, err := afero.ReadFile(runner.FileSystem, "module.yaml") require.NoError(t, err) require.Equal(t, string(expected), string(actual)) }) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml deleted file mode 100644 index e3c4a95a4e..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: aws.yaml - namespace: radius-system -spec: - parameters: '{}' - providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"aws":{"type":"aws","value":{"scope":"awsscope"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' - template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep new file mode 100644 index 0000000000..0261223e02 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep @@ -0,0 +1,11 @@ +extension radius +extension aws + +param bucketName string = 'gkm-bucket' + +resource bucket 'AWS.S3/Bucket@default' = { + alias: bucketName + properties: { + BucketName: bucketName + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml new file mode 100644 index 0000000000..8c13b8bead --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml @@ -0,0 +1,74 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: aws.bicep + namespace: radius-system +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "aws": { + "type": "aws", + "value": { + "scope": "awsscope" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + rootFileName: aws.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "AWS": { + "provider": "AWS", + "version": "latest" + }, + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "4336724644513409792", + "version": "0.30.23.60470" + } + }, + "parameters": { + "bucketName": { + "defaultValue": "gkm-bucket", + "type": "string" + } + }, + "resources": { + "bucket": { + "import": "AWS", + "properties": { + "alias": "[parameters('bucketName')]", + "properties": { + "BucketName": "[parameters('bucketName')]" + } + }, + "type": "AWS.S3/Bucket@default" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml deleted file mode 100644 index 650d876456..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: azure.yaml - namespace: radius-system -spec: - parameters: '{}' - providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"az":{"type":"azure","value":{"scope":"/subscriptions/test-subId/resourceGroups/test-rg"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' - template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep new file mode 100644 index 0000000000..df7267b823 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep @@ -0,0 +1,9 @@ +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { + name: 'gkmstorageaccount' + location: 'eastus' + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: {} +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml new file mode 100644 index 0000000000..6eff218c64 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml @@ -0,0 +1,59 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: azure.bicep + namespace: radius-system +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "az": { + "type": "azure", + "value": { + "scope": "azurescope" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + rootFileName: azure.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "14111843528652336728", + "version": "0.30.23.60470" + } + }, + "resources": { + "storageAccount": { + "apiVersion": "2021-04-01", + "kind": "StorageV2", + "location": "eastus", + "name": "gkmstorageaccount", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "type": "Microsoft.Storage/storageAccounts" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml deleted file mode 100644 index f6760734cb..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: basic.yaml - namespace: radius-system -spec: - parameters: '{}' - providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' - template: '{}' diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep new file mode 100644 index 0000000000..b753c3bb33 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep @@ -0,0 +1,20 @@ +extension radius + +resource basic 'Applications.Core/environments@2023-10-01-preview' = { + name: 'basic' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: 'default' + } + recipes: { + 'Applications.Datastores/redisCaches': { + default: { + templateKind: 'bicep' + templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest' + } + } + } + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml new file mode 100644 index 0000000000..b09c2f74f9 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml @@ -0,0 +1,70 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: basic.bicep + namespace: radius-system +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + rootFileName: basic.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "10886769892319697000", + "version": "0.30.23.60470" + } + }, + "resources": { + "basic": { + "import": "Radius", + "properties": { + "name": "basic", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "default", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep new file mode 100644 index 0000000000..3758e69214 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep @@ -0,0 +1,14 @@ +param location string = resourceGroup().location +param storageAccountName string + +// Import the storage module +module storageModule 'storage.bicep' = { + name: 'storageModule' + params: { + location: location + storageAccountName: storageAccountName + } +} + +// Output the storage account ID +output storageAccountId string = storageModule.outputs.storageAccountId diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml new file mode 100644 index 0000000000..4cd0ad4284 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml @@ -0,0 +1,119 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: module.bicep + namespace: radius-system +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + rootFileName: module.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "1040374933922883026", + "version": "0.30.23.60470" + } + }, + "outputs": { + "storageAccountId": { + "type": "string", + "value": "[reference('storageModule').outputs.storageAccountId.value]" + } + }, + "parameters": { + "location": { + "defaultValue": "[resourceGroup().location]", + "type": "string" + }, + "storageAccountName": { + "type": "string" + } + }, + "resources": { + "storageModule": { + "apiVersion": "2022-09-01", + "name": "storageModule", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "17553429517046312167", + "version": "0.30.23.60470" + } + }, + "outputs": { + "storageAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + } + }, + "parameters": { + "location": { + "type": "string" + }, + "storageAccountName": { + "type": "string" + } + }, + "resources": { + "storageAccount": { + "apiVersion": "2021-04-01", + "kind": "StorageV2", + "location": "[parameters('location')]", + "name": "[parameters('storageAccountName')]", + "properties": {}, + "sku": { + "name": "Standard_LRS" + }, + "type": "Microsoft.Storage/storageAccounts" + } + } + } + }, + "type": "Microsoft.Resources/deployments" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep new file mode 100644 index 0000000000..9a608dfdfc --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep @@ -0,0 +1,14 @@ +param location string +param storageAccountName string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: {} +} + +output storageAccountId string = storageAccount.id diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json new file mode 100644 index 0000000000..15bf94db44 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "b": { + "value": "b" + } + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep new file mode 100644 index 0000000000..6702df6ea9 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep @@ -0,0 +1,23 @@ +extension radius + +param tag string = 'latest' +param kubernetesNamespace string = 'default' + +resource parameters 'Applications.Core/environments@2023-10-01-preview' = { + name: 'parameters' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: kubernetesNamespace + } + recipes: { + 'Applications.Datastores/redisCaches': { + default: { + templateKind: 'bicep' + templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:${tag}' + } + } + } + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json new file mode 100644 index 0000000000..b822e3383d --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "tag": { + "value": "notlatest" + } + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml new file mode 100644 index 0000000000..307ba72c96 --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml @@ -0,0 +1,80 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: parameters.bicep + namespace: radius-system +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + rootFileName: parameters.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "289770176196104222", + "version": "0.30.23.60470" + } + }, + "parameters": { + "kubernetesNamespace": { + "defaultValue": "default", + "type": "string" + }, + "tag": { + "defaultValue": "latest", + "type": "string" + } + }, + "resources": { + "parameters": { + "import": "Radius", + "properties": { + "name": "parameters", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "[parameters('kubernetesNamespace')]", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('ghcr.io/radius-project/recipes/local-dev/rediscaches:{0}', parameters('tag'))]" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml deleted file mode 100644 index 99d24a6dc6..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/value.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: value.yaml - namespace: radius-system -spec: - parameters: '{"kubernetesNamespace":{"value":"test-namespace"}}' - providerConfig: '{"radius":{"type":"radius","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}},"deployments":{"type":"Microsoft.Resources","value":{"scope":"/planes/radius/local/resourceGroups/test-resource-group"}}}' - template: '{"parameters":{"kubernetesNamespace":{}},"resources":[{"some-key":"some-value"}]}' From 0d46bd427dbf4dc5235edab6d24ccb0cdc9696d1 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 10:35:39 -0800 Subject: [PATCH 21/65] Updating functional tests Signed-off-by: willdavsmith --- .../deploymentresource_reconciler.go | 2 +- .../deploymenttemplate_reconciler.go | 18 +- .../noncloud/deploymenttemplate_test.go | 164 +++++++++++------- .../env.bicep} | 0 .../kubernetes/noncloud/testdata/env/env.json | 46 +++++ .../testdata/module/module-dependency.bicep | 17 ++ .../noncloud/testdata/module/module.bicep | 13 ++ .../noncloud/testdata/module/module.json | 103 +++++++++++ .../testdata/tutorial-environment.bicep | 25 --- 9 files changed, 294 insertions(+), 94 deletions(-) rename test/functional-portable/kubernetes/noncloud/testdata/{deploymenttemplate.bicep => env/env.bicep} (100%) create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/env/env.json create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/module/module.json delete mode 100644 test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 89387a3477..cf00aa1f34 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -227,7 +227,7 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // List all DeploymentResource objects in the same namespace // that have the same rootFileName. deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{deploymentResource.Spec.RootFileName: deploymentResource.Spec.RootFileName}) + err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{rootFileNameField: deploymentResource.Spec.RootFileName}) if err != nil { return ctrl.Result{}, nil } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 5573cfd2ac..4755f54b9e 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -235,13 +235,12 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d } } - parameters, err := json.Marshal(deploymentTemplate.Spec.Parameters) + specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) + stringifiedSpecParameters, err := json.MarshalIndent(specParameters, "", " ") if err != nil { return ctrl.Result{}, fmt.Errorf("failed to marshal parameters: %w", err) } - fmt.Println("PARAMETERS: ", string(parameters)) - providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { @@ -254,7 +253,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template - deploymentTemplate.Status.Parameters = string(parameters) + deploymentTemplate.Status.Parameters = string(stringifiedSpecParameters) deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig deploymentTemplate.Status.RootFileName = deploymentTemplate.Spec.RootFileName @@ -411,23 +410,22 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) - parameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) - - stringifiedParameters, err := json.Marshal(parameters) + specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) + stringifiedSpecParameters, err := json.MarshalIndent(specParameters, "", " ") if err != nil { return nil, fmt.Errorf("failed to marshal parameters: %w", err) } // If the resource is already created and is up-to-date, then we don't need to do anything. if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && - deploymentTemplate.Status.Parameters == string(stringifiedParameters) && + deploymentTemplate.Status.Parameters == string(stringifiedSpecParameters) && deploymentTemplate.Status.RootFileName == deploymentTemplate.Spec.RootFileName && deploymentTemplate.Status.ProviderConfig == deploymentTemplate.Spec.ProviderConfig { logger.Info("Resource is already created and is up-to-date.") return nil, nil } - logger.Info("Template, parameters, repository, or providerConfig have changed, starting PUT operation.") + logger.Info("Template, parameters, rootFileName, or providerConfig have changed, starting PUT operation.") var template any err = json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) @@ -460,7 +458,7 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con "mode": "Incremental", "providerConfig": providerConfig, "template": template, - "parameters": parameters, + "parameters": specParameters, } resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index e01d473079..ddaff9de46 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -19,8 +19,7 @@ package kubernetes_test import ( "context" "encoding/json" - "fmt" - "strings" + "path" "testing" "time" @@ -29,6 +28,7 @@ import ( radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" "github.com/radius-project/radius/pkg/controller/reconciler" "github.com/radius-project/radius/pkg/sdk" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/test/rp" "github.com/radius-project/radius/test/testcontext" "github.com/spf13/afero" @@ -45,80 +45,105 @@ import ( controller_runtime "sigs.k8s.io/controller-runtime/pkg/client" ) -func Test_DeploymentTemplate_K8sManifest(t *testing.T) { - ctx := testcontext.New(t) - opts := rp.NewRPTestOptions(t) - - name := "dt" - namespace := "kubernetes-deploymenttemplate-test" - - template, err := afero.ReadFile(afero.NewOsFs(), "testdata/deploymenttemplate.bicep") +func Test_DeploymentTemplate(t *testing.T) { + defaultProviderConfig, err := generateDefaultProviderConfig() require.NoError(t, err) - paramsList := []string{ - fmt.Sprintf("name=%s", name), - fmt.Sprintf("namespace=%s", namespace), + testcases := []struct { + name string + namespace string + fileName string + templateFilePath string + providerConfig string + parameters map[string]string + }{ + { + name: "dt-env", + namespace: "dt-ns-env", + fileName: "env.bicep", + templateFilePath: path.Join("testdata", "env", "env.json"), + providerConfig: defaultProviderConfig, + parameters: map[string]string{ + "name": "dt-env", + "namespace": "dt-ns-env", + }, + }, + { + name: "dt-module", + namespace: "dt-ns-module", + fileName: "module.bicep", + templateFilePath: path.Join("testdata", "module", "module.json"), + providerConfig: defaultProviderConfig, + parameters: map[string]string{ + "name": "dt-module", + "namespace": "dt-ns-module", + }, + }, } - parametersMap := make(map[string]any, len(paramsList)) - for _, param := range paramsList { - parts := strings.SplitN(param, "=", 2) - parametersMap[parts[0]] = parts[1] - } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + ctx := testcontext.New(t) + opts := rp.NewRPTestOptions(t) - parameters, err := json.Marshal(parametersMap) - require.NoError(t, err) + name := tc.name + namespace := tc.namespace + + template, err := afero.ReadFile(afero.NewOsFs(), tc.templateFilePath) + require.NoError(t, err) - // Create the namespace, if it already exists we can ignore the error. - _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) - require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) + // Create the namespace, if it already exists we can ignore the error. + _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) - deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), string(parameters), "{}", "") + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), tc.providerConfig, tc.fileName, tc.parameters) - t.Run("Deploy", func(t *testing.T) { - t.Log("Creating DeploymentTemplate") - err = opts.Client.Create(ctx, deploymentTemplate) - require.NoError(t, err) - }) + t.Run("Deploy", func(t *testing.T) { + t.Log("Creating DeploymentTemplate") + err = opts.Client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + }) - t.Run("Check status", func(t *testing.T) { - ctx, cancel := testcontext.NewWithCancel(t) - defer cancel() + t.Run("Check status", func(t *testing.T) { + ctx, cancel := testcontext.NewWithCancel(t) + defer cancel() - // Get resource version - err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) - require.NoError(t, err) + // Get resource version + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + require.NoError(t, err) - t.Log("Waiting for DeploymentTemplate ready") - deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) - require.NoError(t, err) + t.Log("Waiting for DeploymentTemplate ready") + deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) + require.NoError(t, err) - // Doing a basic check that the deploymentTemplate has a resource provisioned. - require.NotEmpty(t, deploymentTemplate.Status.Resource) + // Doing a basic check that the deploymentTemplate has a resource provisioned. + require.NotEmpty(t, deploymentTemplate.Status.Resource) - scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) - require.NoError(t, err) + scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + require.NoError(t, err) - client, err := generated.NewGenericResourcesClient(scope, "Applications.Core/environments", &aztoken.AnonymousCredential{}, sdk.NewClientOptions(opts.Connection)) - require.NoError(t, err) + client, err := generated.NewGenericResourcesClient(scope, "Applications.Core/environments", &aztoken.AnonymousCredential{}, sdk.NewClientOptions(opts.Connection)) + require.NoError(t, err) - _, err = client.Get(ctx, deploymentTemplate.Name, nil) - require.NoError(t, err) - }) + _, err = client.Get(ctx, deploymentTemplate.Name, nil) + require.NoError(t, err) + }) - t.Run("Delete", func(t *testing.T) { - t.Log("Deleting DeploymentTemplate") - err = opts.Client.Delete(ctx, deploymentTemplate) - require.NoError(t, err) + t.Run("Delete", func(t *testing.T) { + t.Log("Deleting DeploymentTemplate") + err = opts.Client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) - require.Eventually(t, func() bool { - err = opts.Client.Get(ctx, types.NamespacedName{Name: "db", Namespace: namespace}, deploymentTemplate) - return apierrors.IsNotFound(err) - }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") - }) + require.Eventually(t, func() bool { + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + return apierrors.IsNotFound(err) + }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }) + }) + } } -func makeDeploymentTemplate(name types.NamespacedName, template, parameters, providerConfig, repository string) *radappiov1alpha3.DeploymentTemplate { +func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig, rootFileName string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: name.Name, @@ -128,7 +153,7 @@ func makeDeploymentTemplate(name types.NamespacedName, template, parameters, pro Template: template, Parameters: parameters, ProviderConfig: providerConfig, - Repository: repository, + RootFileName: rootFileName, }, } @@ -173,3 +198,26 @@ func waitForDeploymentTemplateReady(t *testing.T, ctx context.Context, name type } } } + +func generateDefaultProviderConfig() (string, error) { + providerConfig := sdkclients.ProviderConfig{} + + providerConfig.Radius = &sdkclients.Radius{ + Type: "radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + } + providerConfig.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourceGroups/default", + }, + } + + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + if err != nil { + return "", err + } + return string(marshalledProviderConfig), nil +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep b/test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep similarity index 100% rename from test/functional-portable/kubernetes/noncloud/testdata/deploymenttemplate.bicep rename to test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep diff --git a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json new file mode 100644 index 0000000000..7a2c23cbe5 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "10640932074887270592" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "[parameters('name')]", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('namespace')]" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep new file mode 100644 index 0000000000..08a0860458 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep @@ -0,0 +1,17 @@ +extension radius + +param name string +param namespace string + +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: name + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: namespace + } + } +} + +output envId string = env.id diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep new file mode 100644 index 0000000000..2f8bb39528 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep @@ -0,0 +1,13 @@ +param name string +param namespace string + +module module 'module-dependency.bicep' = { + name: 'module' + params: { + name: name + namespace: namespace + } +} + +// Output the storage account ID +output envId string = module.outputs.envId diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json new file mode 100644 index 0000000000..2ba9b18914 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "17470534345050893154" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "resources": { + "module": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "module", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "namespace": { + "value": "[parameters('namespace')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "13226276940735477072" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "[parameters('name')]", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('namespace')]" + } + } + } + } + }, + "outputs": { + "envId": { + "type": "string", + "value": "[reference('env').id]" + } + } + } + } + } + }, + "outputs": { + "envId": { + "type": "string", + "value": "[reference('module').outputs.envId.value]" + } + } +} \ No newline at end of file diff --git a/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep b/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep deleted file mode 100644 index 1097692274..0000000000 --- a/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep +++ /dev/null @@ -1,25 +0,0 @@ -extension radius - -param name string -param namespace string -param registry string -param version string - -resource env 'Applications.Core/environments@2023-10-01-preview' = { - name: name - properties: { - compute: { - kind: 'kubernetes' - resourceId: 'self' - namespace: namespace - } - recipes: { - 'Applications.Datastores/redisCaches': { - default: { - templateKind: 'bicep' - templatePath: '${registry}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:${version}' - } - } - } - } -} From 66927eecf52d7084e8ad110128b15a5b8ef7a073 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 11:14:28 -0800 Subject: [PATCH 22/65] PR Signed-off-by: willdavsmith --- .../crds/radius/radapp.io_deploymentresources.yaml | 10 +++++++--- .../crds/radius/radapp.io_deploymenttemplates.yaml | 12 ++++++------ deploy/Chart/crds/radius/radapp.io_recipes.yaml | 2 +- deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml | 2 +- deploy/Chart/crds/ucpd/ucp.dev_resources.yaml | 2 +- go.mod | 2 -- go.sum | 5 +++++ .../radapp.io/v1alpha3/deploymentresource_types.go | 5 +++-- .../radapp.io/v1alpha3/deploymenttemplate_types.go | 6 +++--- 9 files changed, 27 insertions(+), 19 deletions(-) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 85e36997dd..18f501958d 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.4 + controller-gen.kubebuilder.io/version: v0.16.0 name: deploymentresources.radapp.io spec: group: radapp.io @@ -22,6 +22,10 @@ spec: jsonPath: .status.phrase name: Status type: string + - description: Name of the Bicep file that bicep build is run on + jsonPath: .status.rootFileName + name: RootFileName + type: string name: v1alpha3 schema: openAPIV3Schema: @@ -56,7 +60,7 @@ spec: type: string rootFileName: description: |- - RootFileName is the name of the Bicep file in the repository that + RootFileName is the name of the Bicep file that `bicep build` is run on. type: string type: object @@ -97,7 +101,7 @@ spec: type: string rootFileName: description: |- - RootFileName is the name of the Bicep file in the repository that + RootFileName is the name of the Bicep file that `bicep build` is run on. type: string type: object diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index d38657b19e..35469029f1 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.4 + controller-gen.kubebuilder.io/version: v0.16.0 name: deploymenttemplates.radapp.io spec: group: radapp.io @@ -22,9 +22,9 @@ spec: jsonPath: .status.phrase name: Status type: string - - description: Repository of the resource - jsonPath: .status.repository - name: Repository + - description: Name of the Bicep file that bicep build is run on + jsonPath: .status.rootFileName + name: RootFileName type: string name: v1alpha3 schema: @@ -62,7 +62,7 @@ spec: type: string rootFileName: description: |- - RootFileName is the name of the Bicep file in the repository that + RootFileName is the name of the Bicep file that `bicep build` is run on. type: string template: @@ -116,7 +116,7 @@ spec: type: string rootFileName: description: |- - RootFileName is the name of the Bicep file in the repository that + RootFileName is the name of the Bicep file that `bicep build` is run on. type: string template: diff --git a/deploy/Chart/crds/radius/radapp.io_recipes.yaml b/deploy/Chart/crds/radius/radapp.io_recipes.yaml index 039c7ff66e..fb2bf92974 100644 --- a/deploy/Chart/crds/radius/radapp.io_recipes.yaml +++ b/deploy/Chart/crds/radius/radapp.io_recipes.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.4 + controller-gen.kubebuilder.io/version: v0.16.0 name: recipes.radapp.io spec: group: radapp.io diff --git a/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml b/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml index 925ce30e1f..03dda2fcd1 100644 --- a/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml +++ b/deploy/Chart/crds/ucpd/ucp.dev_queuemessages.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.4 + controller-gen.kubebuilder.io/version: v0.16.0 name: queuemessages.ucp.dev spec: group: ucp.dev diff --git a/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml b/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml index bede700f8e..d4ab40029d 100644 --- a/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml +++ b/deploy/Chart/crds/ucpd/ucp.dev_resources.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.4 + controller-gen.kubebuilder.io/version: v0.16.0 name: resources.ucp.dev spec: group: ucp.dev diff --git a/go.mod b/go.mod index e454a9c688..50a7005cd9 100644 --- a/go.mod +++ b/go.mod @@ -149,8 +149,6 @@ require ( github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect - github.com/onsi/ginkgo/v2 v2.20.1 // indirect - github.com/onsi/gomega v1.34.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/go.sum b/go.sum index 67b8320507..e55fa7d39b 100644 --- a/go.sum +++ b/go.sum @@ -402,12 +402,17 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= <<<<<<< HEAD +<<<<<<< HEAD github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= ======= github.com/cyphar/filepath-securejoin v0.3.2 h1:QhZu5AxQ+o1XZH0Ye05YzvJ0kAdK6VQc0z9NNMek7gc= github.com/cyphar/filepath-securejoin v0.3.2/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= >>>>>>> 88eea07d2 (PR) +======= +github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= +github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= +>>>>>>> ad721fdcb (PR) github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 50b7a87ff8..bf217ea605 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -28,7 +28,7 @@ type DeploymentResourceSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // RootFileName is the name of the Bicep file in the repository that + // RootFileName is the name of the Bicep file that // `bicep build` is run on. RootFileName string `json:"rootFileName,omitempty"` } @@ -41,7 +41,7 @@ type DeploymentResourceStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // RootFileName is the name of the Bicep file in the repository that + // RootFileName is the name of the Bicep file that // `bicep build` is run on. RootFileName string `json:"rootFileName,omitempty"` @@ -79,6 +79,7 @@ const ( // +kubebuilder:subresource:status // +kubebuilder:resource:categories={"all","radius"} // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" +// +kubebuilder:printcolumn:name="RootFileName",type="string",JSONPath=".status.rootFileName",description="Name of the Bicep file that bicep build is run on" // DeploymentResource is the Schema for the DeploymentResources API type DeploymentResource struct { diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 183ac18599..178657d30c 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -31,7 +31,7 @@ type DeploymentTemplateSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // RootFileName is the name of the Bicep file in the repository that + // RootFileName is the name of the Bicep file that // `bicep build` is run on. RootFileName string `json:"rootFileName,omitempty"` } @@ -50,7 +50,7 @@ type DeploymentTemplateStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // RootFileName is the name of the Bicep file in the repository that + // RootFileName is the name of the Bicep file that // `bicep build` is run on. RootFileName string `json:"rootFileName,omitempty"` @@ -94,7 +94,7 @@ const ( // +kubebuilder:subresource:status // +kubebuilder:resource:categories={"all","radius"} // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" -// +kubebuilder:printcolumn:name="Repository",type="string",JSONPath=".status.repository",description="Repository of the resource" +// +kubebuilder:printcolumn:name="RootFileName",type="string",JSONPath=".status.rootFileName",description="Name of the Bicep file that bicep build is run on" // DeploymentTemplate is the Schema for the deploymenttemplates API type DeploymentTemplate struct { From db96f40fcf64aa3c7d3a276acd06988a0d1d0575 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 11:25:18 -0800 Subject: [PATCH 23/65] PR Signed-off-by: willdavsmith --- go.mod | 18 +----------------- go.sum | 30 ------------------------------ 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/go.mod b/go.mod index 50a7005cd9..692107b8fc 100644 --- a/go.mod +++ b/go.mod @@ -30,17 +30,7 @@ require ( github.com/charmbracelet/x/ansi v0.4.5 github.com/charmbracelet/x/exp/teatest v0.0.0-20240408110044-525ba71bb562 github.com/dimchansky/utfbom v1.1.1 -<<<<<<< HEAD github.com/fatih/color v1.18.0 -======= - github.com/fatih/color v1.17.0 -<<<<<<< HEAD - github.com/fluxcd/pkg/http/fetch v0.12.1 - github.com/fluxcd/pkg/tar v0.8.1 - github.com/fluxcd/source-controller/api v1.4.1 ->>>>>>> 88eea07d2 (PR) -======= ->>>>>>> ff7b44061 (removing fluxcontroller) github.com/go-chi/chi/v5 v5.1.0 github.com/go-git/go-git/v5 v5.12.0 github.com/go-logr/logr v1.4.2 @@ -339,15 +329,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect -<<<<<<< HEAD - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 k8s.io/apiserver v0.31.2 // indirect k8s.io/component-base v0.31.2 // indirect -======= - gopkg.in/yaml.v2 v2.4.0 - k8s.io/apiserver v0.31.1 // indirect - k8s.io/component-base v0.31.1 // indirect ->>>>>>> ff7b44061 (removing fluxcontroller) k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect diff --git a/go.sum b/go.sum index e55fa7d39b..5deffccdc8 100644 --- a/go.sum +++ b/go.sum @@ -401,18 +401,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -<<<<<<< HEAD -<<<<<<< HEAD github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= -======= -github.com/cyphar/filepath-securejoin v0.3.2 h1:QhZu5AxQ+o1XZH0Ye05YzvJ0kAdK6VQc0z9NNMek7gc= -github.com/cyphar/filepath-securejoin v0.3.2/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= ->>>>>>> 88eea07d2 (PR) -======= -github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= -github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= ->>>>>>> ad721fdcb (PR) github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -869,33 +859,13 @@ github.com/novln/docker-parser v1.0.0 h1:PjEBd9QnKixcWczNGyEdfUrP6GR0YUilAqG7Wks github.com/novln/docker-parser v1.0.0/go.mod h1:oCeM32fsoUwkwByB5wVjsrsVQySzPWkl3JdlTn1txpE= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -<<<<<<< HEAD github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -<<<<<<< HEAD github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -======= -github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= -github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be h1:f2PlhC9pm5sqpBZFvnAoKj+KzXRzbjFMA+TqXfJdgho= -github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= ->>>>>>> 88eea07d2 (PR) -github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98 h1:LTxrNWOPwquJy9Cu3oz6QHJIO5M5gNyOZtSybXdyLA4= -github.com/opencontainers/go-digest/blake3 v0.0.0-20231025023718-d50d2fec9c98/go.mod h1:kqQaIc6bZstKgnGpL7GD5dWoLKbA6mH1Y9ULjGImBnM= -======= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= ->>>>>>> ff7b44061 (removing fluxcontroller) github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= From b57d9c4ef87e7e76ef1e68b74edcbe97785b85ba Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 12:00:48 -0800 Subject: [PATCH 24/65] PR Signed-off-by: willdavsmith --- .../generatekubernetesmanifest.go | 2 +- pkg/cli/cmd/deploy/deploy.go | 2 +- .../kubernetes/noncloud/testdata/env/env.json | 6 ++---- .../kubernetes/noncloud/testdata/module/module.json | 10 +++------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 7a12b66d63..ceb1ebbfec 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -58,7 +58,7 @@ func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/parameter-files You can specify parameters using multiple sources. Parameters can be overridden based on the - order the are provided. Parameters appearing later in the argument list will override those defined earlier. + order they are provided. Parameters appearing later in the argument list will override those defined earlier. `, Example: ` # Generate a DeploymentTemplate Custom Resource from a Bicep file. diff --git a/pkg/cli/cmd/deploy/deploy.go b/pkg/cli/cmd/deploy/deploy.go index bc4f0a9df0..b2216d1f9d 100644 --- a/pkg/cli/cmd/deploy/deploy.go +++ b/pkg/cli/cmd/deploy/deploy.go @@ -70,7 +70,7 @@ When passing multiple parameters in a single file, use the format described here https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/parameter-files You can specify parameters using multiple sources. Parameters can be overridden based on the -order the are provided. Parameters appearing later in the argument list will override those defined earlier. +order they are provided. Parameters appearing later in the argument list will override those defined earlier. `, Example: ` # deploy a Bicep template diff --git a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json index 7a2c23cbe5..5fc46b7113 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.31.92.45157", @@ -43,4 +41,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index 2ba9b18914..9e2a2654b6 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.31.92.45157", @@ -45,9 +43,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.31.92.45157", @@ -100,4 +96,4 @@ "value": "[reference('module').outputs.envId.value]" } } -} \ No newline at end of file +} From 17a501fbdb06525f2889d53cf2c37c797eb7a97f Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 12:02:44 -0800 Subject: [PATCH 25/65] PR Signed-off-by: willdavsmith --- bicep-types | 2 +- build/build.mk | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bicep-types b/bicep-types index 96b34cbb74..0eb4785159 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 96b34cbb749f791c2b5b72f83d448568942aeb27 +Subproject commit 0eb478515986e790b522f136756c0406ad3b698a diff --git a/build/build.mk b/build/build.mk index 7f4016cfdb..f173721c5b 100644 --- a/build/build.mk +++ b/build/build.mk @@ -153,3 +153,4 @@ clean: ## Cleans output directory. .PHONY: lint lint: ## Runs golangci-lint $(GOLANGCI_LINT) run --fix --timeout 5m + From a85eb274e693529cb8c51591ca84484d10b8cdac Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 12:07:59 -0800 Subject: [PATCH 26/65] PR Signed-off-by: willdavsmith --- deploy/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/install.sh b/deploy/install.sh index bd3a074fb1..08d00285d7 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -125,7 +125,7 @@ downloadFile() { exit 1 fi - DOWNLOAD_URL="ghcr.io/willdavsmith/flux-demo/rad/${OS}-${ARCH}:latest" + DOWNLOAD_URL="ghcr.io/radius-project/rad/${OS}-${ARCH}:latest" echo "Downloading edge CLI from ${DOWNLOAD_URL}..." oras pull $DOWNLOAD_URL -o $RADIUS_TMP_ROOT From ffc83af6ee47232e3fc769c5eb9098f7fdc08caa Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 12:20:57 -0800 Subject: [PATCH 27/65] PR Signed-off-by: willdavsmith --- .../testdata/tutorial-environment.bicep | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep diff --git a/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep b/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep new file mode 100644 index 0000000000..1097692274 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/tutorial-environment.bicep @@ -0,0 +1,25 @@ +extension radius + +param name string +param namespace string +param registry string +param version string + +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: name + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: namespace + } + recipes: { + 'Applications.Datastores/redisCaches': { + default: { + templateKind: 'bicep' + templatePath: '${registry}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:${version}' + } + } + } + } +} From be204ceac2f948968d74c8e031b315eecf22d593 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 12:24:48 -0800 Subject: [PATCH 28/65] PR Signed-off-by: willdavsmith --- test/radcli/cli.go | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/test/radcli/cli.go b/test/radcli/cli.go index 67e5cd6a59..0b548607cb 100644 --- a/test/radcli/cli.go +++ b/test/radcli/cli.go @@ -287,33 +287,6 @@ func (cli *CLI) BicepPublish(ctx context.Context, file, target string) (string, return cli.RunCommand(ctx, args) } -func (cli *CLI) BicepGenerateKubernetesManifest(ctx context.Context, templateFilePath, outfile, environment string, parameters ...string) error { - // Check if the template file path exists - if _, err := os.Stat(templateFilePath); err != nil { - return fmt.Errorf("could not find template file: %s - %w", templateFilePath, err) - } - - args := []string{ - "bicep", - "generate-kubernetes-manifest", - templateFilePath, - } - - if environment != "" { - args = append(args, "--environment", environment) - } - - if outfile != "" { - args = append(args, "--outfile", outfile) - } - - for _, parameter := range parameters { - args = append(args, "--parameters", parameter) - } - _, err := cli.RunCommand(ctx, args) - return err -} - // Version runs the version command and returns the output as a string, or an error if the command fails. func (cli *CLI) Version(ctx context.Context) (string, error) { args := []string{ From bb34c5684d0d502cbdf7dc403086d9eb26afa7cf Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 21 Nov 2024 16:21:36 -0800 Subject: [PATCH 29/65] fix tests Signed-off-by: willdavsmith --- .../deploymenttemplate_reconciler_test.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 3b3db3fbf0..a08e210299 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -39,9 +39,9 @@ const ( DeploymentTemplateTestWaitInterval = time.Second * 1 DeploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 - TestDeploymentTemplateNamespace = "DeploymentTemplate-basic" - TestDeploymentTemplateName = "test-DeploymentTemplate" - TestDeploymentTemplateRadiusResourceGroup = "default-DeploymentTemplate-basic" + TestDeploymentTemplateNamespace = "basic" + TestDeploymentTemplateName = "test-deploymenttemplate" + TestDeploymentTemplateRadiusResourceGroup = "default-basic" ) var ( @@ -72,7 +72,7 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client err = (&DeploymentTemplateReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("DeploymentTemplate-controller"), + EventRecorder: mgr.GetEventRecorderFor("controller"), Radius: radius, DelayInterval: DeploymentTemplateTestControllerDelayInterval, }).SetupWithManager(mgr) @@ -90,7 +90,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) radius, client := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "DeploymentTemplate-basic", Name: "test-DeploymentTemplate"} + name := types.NamespacedName{Namespace: "basic", Name: "test-deploymenttemplate"} err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) @@ -106,13 +106,13 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", scope) + require.Equal(t, "/planes/radius/local/resourcegroups/default-basic", scope) radius.CompleteOperation(status.Operation.ResumeToken, nil) // Deployment will update after operation completes status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic/providers/Microsoft.Resources/deployments/test-DeploymentTemplate", status.Resource) + require.Equal(t, "/planes/radius/local/resourcegroups/default-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate", status.Resource) resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) require.NoError(t, err) @@ -124,13 +124,13 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { "deployments": map[string]any{ "type": "Microsoft.Resources", "value": map[string]any{ - "scope": "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", + "scope": "/planes/radius/local/resourcegroups/default-basic", }, }, "radius": map[string]any{ "type": "Radius", "value": map[string]any{ - "scope": "/planes/radius/local/resourcegroups/default-DeploymentTemplate-basic", + "scope": "/planes/radius/local/resourcegroups/default-basic", }, }, }, "template": map[string]any{}, @@ -157,7 +157,7 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { ctx := testcontext.New(t) radius, client := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "DeploymentTemplate-failure-recovery", Name: "test-DeploymentTemplate-failure-recovery"} + name := types.NamespacedName{Namespace: "failure-recovery", Name: "test-failure-recovery"} err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) From d814bcd4a452878675e748188765ece284616abb Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 5 Dec 2024 11:00:56 -0800 Subject: [PATCH 30/65] deps Signed-off-by: willdavsmith --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b44318e94e..c6488f45bb 100644 --- a/go.mod +++ b/go.mod @@ -329,7 +329,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v2 v2.4.0 k8s.io/apiserver v0.31.3 // indirect k8s.io/component-base v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect From df2a9d020684941635437a94974e5fadc2574cab Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 6 Dec 2024 08:57:23 -0800 Subject: [PATCH 31/65] Fixing tests Signed-off-by: willdavsmith --- .../reconciler/deployment_reconciler_test.go | 10 +- .../deploymentresource_reconciler.go | 62 +++-- .../deploymentresource_reconciler_test.go | 12 +- .../deploymenttemplate_reconciler.go | 146 ++++++----- .../deploymenttemplate_reconciler_test.go | 241 ++++++++++++------ pkg/controller/reconciler/mock_client_test.go | 7 + .../reconciler/recipe_reconciler_test.go | 10 +- .../reconciler/recipe_webhook_test.go | 2 +- pkg/controller/reconciler/shared_test.go | 43 +++- .../deploymenttemplate-withresources.json | 39 +++ 10 files changed, 370 insertions(+), 202 deletions(-) create mode 100644 pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json diff --git a/pkg/controller/reconciler/deployment_reconciler_test.go b/pkg/controller/reconciler/deployment_reconciler_test.go index 68e6a597f6..e60f7e332d 100644 --- a/pkg/controller/reconciler/deployment_reconciler_test.go +++ b/pkg/controller/reconciler/deployment_reconciler_test.go @@ -108,7 +108,7 @@ func Test_DeploymentReconciler_RadiusEnabled_ThenDeploymentDeleted(t *testing.T) require.NoError(t, err) // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Deployment will be waiting for container to complete deployment. annotations := waitForStateUpdating(t, client, name) @@ -154,7 +154,7 @@ func Test_DeploymentReconciler_ChangeEnvironmentAndApplication(t *testing.T) { require.NoError(t, err) // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Deployment will be waiting for container to complete deployment. annotations := waitForStateUpdating(t, client, name) @@ -167,7 +167,7 @@ func Test_DeploymentReconciler_ChangeEnvironmentAndApplication(t *testing.T) { annotations = waitForStateReady(t, client, name) require.Equal(t, "/planes/radius/local/resourcegroups/default-deployment-change-envapp/providers/Applications.Core/containers/test-deployment-change-envapp", annotations.Status.Container) - createEnvironment(radius, "new-environment") + createEnvironment(radius, "new-environment", "default") // Now update the deployment to change the environment and application. err = client.Get(ctx, name, deployment) @@ -228,7 +228,7 @@ func Test_DeploymentReconciler_RadiusEnabled_ThenRadiusDisabled(t *testing.T) { require.NoError(t, err) // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Deployment will be waiting for container to complete deployment. annotations := waitForStateUpdating(t, client, name) @@ -283,7 +283,7 @@ func Test_DeploymentReconciler_Connections(t *testing.T) { require.NoError(t, err) // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Deployment will be waiting for recipe resources to be created _ = waitForStateWaiting(t, client, name) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index cf00aa1f34..793d0e1fb7 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -112,10 +112,12 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R return r.reconcileDelete(ctx, &deploymentResource) } - // If we get here then it means we can process the result of the operation. logger.Info("Resource is in desired state.", "resourceId", deploymentResource.Spec.Id) deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseReady + deploymentResource.Status.ProviderConfig = deploymentResource.Spec.ProviderConfig + deploymentResource.Status.RootFileName = deploymentResource.Spec.RootFileName + deploymentResource.Status.Id = deploymentResource.Spec.Id err = r.Client.Status().Update(ctx, &deploymentResource) if err != nil { return ctrl.Result{}, err @@ -130,23 +132,10 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - providerConfig := sdkclients.ProviderConfig{ - Radius: &sdkclients.Radius{ - Type: "radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - }, - Deployments: &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - }, - } + providerConfig := sdkclients.ProviderConfig{} err := json.Unmarshal([]byte(deploymentResource.Spec.ProviderConfig), &providerConfig) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to unmarshal providerConfig: %w", err) } poller, err := r.Radius.Resources(providerConfig.Deployments.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) @@ -169,6 +158,17 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d if clients.Is404Error(err) { // The resource was not found, so we can consider it deleted. logger.Info("Resource was not found.") + + // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the + // DeploymentResource + if controllerutil.RemoveFinalizer(deploymentResource, DeploymentResourceFinalizer) { + deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + err = r.Client.Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } @@ -187,9 +187,25 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d } // If we get here, the operation was a success. Update the status and continue. - // - // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentResource.Status.Operation = nil + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + err = r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + + logger.Info("Resource is deleted.") + + // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the + // DeploymentResource + if controllerutil.RemoveFinalizer(deploymentResource, DeploymentResourceFinalizer) { + deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleted + err = r.Client.Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } @@ -253,11 +269,6 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // dont delete app until otherCount is 0 if otherCount > 0 { logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } } @@ -265,11 +276,6 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { if otherCount > 0 { logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } } diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index a8499d8c3a..3c52bbeb4d 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -70,7 +70,7 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client err = (&DeploymentResourceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("DeploymentResource-controller"), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), Radius: radius, DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) @@ -86,7 +86,7 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client func Test_DeploymentResourceReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) - radius, client := SetupDeploymentResourceTest(t) + _, client := SetupDeploymentResourceTest(t) name := types.NamespacedName{Namespace: TestDeploymentResourceNamespace, Name: TestDeploymentResourceName} err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) @@ -103,10 +103,6 @@ func Test_DeploymentResourceReconciler_Basic(t *testing.T) { err = client.Delete(ctx, deployment) require.NoError(t, err) - // Deletion of the DeploymentResource is in progress. - status = waitForDeploymentResourceStateDeleting(t, client, name, nil) - radius.CompleteOperation(status.Operation.ResumeToken, nil) - // Now deleting of the DeploymentResource object can complete. waitForDeploymentResourceDeleted(t, client, name) } @@ -124,12 +120,10 @@ func waitForDeploymentResourceStateReady(t *testing.T, client client.Client, nam status = ¤t.Status logger.Logf("DeploymentResource.Status: %+v", current.Status) - assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") - if assert.Equal(t, radappiov1alpha3.DeploymentResourcePhraseReady, current.Status.Phrase) { assert.Empty(t, current.Status.Operation) } - }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "failed to enter updating state") + }, DeploymentResourceTestWaitDuration, DeploymentResourceTestWaitInterval, "failed to enter ready state") return status } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 4755f54b9e..c7838ac654 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -125,7 +125,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Status.ProviderConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to parse deployment scope: %w", err) } @@ -153,6 +153,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + deploymentTemplate.Status.Message = err.Error() err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -165,72 +166,74 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d // Get outputResources from the response outputResources := make([]string, 0) - outputResourceList := resp.Properties["outputResources"].([]any) - for _, resource := range outputResourceList { - outputResource := resource.(map[string]any) - outputResources = append(outputResources, outputResource["id"].(string)) - } - - // Compare outputResources with existing DeploymentResources - // if is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it - // if is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it - // if is present in both, do nothing + if resp.Properties["outputResources"] != nil { + outputResourceList := resp.Properties["outputResources"].([]any) + for _, resource := range outputResourceList { + outputResource := resource.(map[string]any) + outputResources = append(outputResources, outputResource["id"].(string)) + } - existingOutputResources := make(map[string]bool) - for _, resource := range deploymentTemplate.Status.OutputResources { - existingOutputResources[resource] = true - } + // Compare outputResources with existing DeploymentResources + // if is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + // if is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + // if is present in both, do nothing - newOutputResources := make(map[string]bool) - for _, resource := range outputResources { - newOutputResources[resource] = true - } + existingOutputResources := make(map[string]bool) + for _, resource := range deploymentTemplate.Status.OutputResources { + existingOutputResources[resource] = true + } - for _, outputResourceId := range outputResources { - if _, ok := existingOutputResources[outputResourceId]; !ok { - // Resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it - - resourceName := generateDeploymentResourceName(outputResourceId) - deploymentResource := &radappiov1alpha3.DeploymentResource{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: deploymentTemplate.Namespace, - }, - Spec: radappiov1alpha3.DeploymentResourceSpec{ - Id: outputResourceId, - ProviderConfig: deploymentTemplate.Spec.ProviderConfig, - RootFileName: deploymentTemplate.Spec.RootFileName, - }, - } + newOutputResources := make(map[string]bool) + for _, resource := range outputResources { + newOutputResources[resource] = true + } - if controllerutil.AddFinalizer(deploymentResource, DeploymentResourceFinalizer) { - // Add the DeploymentTemplate as the owner of the DeploymentResource - if err := controllerutil.SetControllerReference(deploymentTemplate, deploymentResource, r.Scheme); err != nil { - return ctrl.Result{}, err + for _, outputResourceId := range outputResources { + if _, ok := existingOutputResources[outputResourceId]; !ok { + // Resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + + resourceName := generateDeploymentResourceName(outputResourceId) + deploymentResource := &radappiov1alpha3.DeploymentResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: deploymentTemplate.Namespace, + }, + Spec: radappiov1alpha3.DeploymentResourceSpec{ + Id: outputResourceId, + ProviderConfig: deploymentTemplate.Spec.ProviderConfig, + RootFileName: deploymentTemplate.Spec.RootFileName, + }, } - // Create the DeploymentResource - err = r.Client.Create(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err + if controllerutil.AddFinalizer(deploymentResource, DeploymentResourceFinalizer) { + // Add the DeploymentTemplate as the owner of the DeploymentResource + if err := controllerutil.SetControllerReference(deploymentTemplate, deploymentResource, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + // Create the DeploymentResource + err = r.Client.Create(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } } } } - } - for _, resource := range deploymentTemplate.Status.OutputResources { - if _, ok := newOutputResources[resource]; !ok { - // Resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it - logger.Info("Deleting resource.", "resourceId", resource) - resourceName := generateDeploymentResourceName(resource) - err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: deploymentTemplate.Namespace, - }, - }) - if err != nil { - return ctrl.Result{}, err + for _, resource := range deploymentTemplate.Status.OutputResources { + if _, ok := newOutputResources[resource]; !ok { + // Resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + logger.Info("Deleting resource.", "resourceId", resource) + resourceName := generateDeploymentResourceName(resource) + err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: deploymentTemplate.Namespace, + }, + }) + if err != nil { + return ctrl.Result{}, err + } } } } @@ -244,7 +247,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal template: %w", err) + return ctrl.Result{}, fmt.Errorf("failed to unmarshal providerConfig: %w", err) } // If we get here, the operation was a success. Update the status and continue. @@ -255,18 +258,18 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template deploymentTemplate.Status.Parameters = string(stringifiedSpecParameters) deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name - deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig - deploymentTemplate.Status.RootFileName = deploymentTemplate.Spec.RootFileName return ctrl.Result{}, nil } // If we get here, this was an unknown operation kind. This is a bug in our code, or someone // tampered with the status of the object. Just reset the state and move on. - logger.Error(fmt.Errorf("unknown operation kind: %s", deploymentTemplate.Status.Operation.OperationKind), "Unknown operation kind.") + errorMessage := fmt.Errorf("unknown operation kind: %s", deploymentTemplate.Status.Operation.OperationKind) + logger.Error(errorMessage, "Unknown operation kind.") deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + deploymentTemplate.Status.Message = errorMessage.Error() err := r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -294,10 +297,20 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation + deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig + deploymentTemplate.Status.RootFileName = deploymentTemplate.Spec.RootFileName + updatePoller, err := r.startPutOperationIfNeeded(ctx, deploymentTemplate) if err != nil { logger.Error(err, "Unable to create or update resource.") r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) + deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed + deploymentTemplate.Status.Message = err.Error() + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, err } else if updatePoller != nil { // We've successfully started an operation. Update the status and requeue. @@ -339,12 +352,15 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleting + err := r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } // List all DeploymentResource objects in the same namespace deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentTemplate.Namespace)) + err = r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentTemplate.Namespace)) if err != nil { return ctrl.Result{}, nil } @@ -425,7 +441,7 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con return nil, nil } - logger.Info("Template, parameters, rootFileName, or providerConfig have changed, starting PUT operation.") + logger.Info("Template, Parameters, RootFileName, or ProviderConfig have changed, starting PUT operation.") var template any err = json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) @@ -436,7 +452,7 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { - return nil, fmt.Errorf("failed to unmarshal template: %w", err) + return nil, fmt.Errorf("failed to unmarshal providerConfig: %w", err) } if providerConfig.Deployments == nil { return nil, fmt.Errorf("providerConfig.Deployments is nil") diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index a08e210299..240c3539b7 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -17,13 +17,16 @@ limitations under the License. package reconciler import ( + "encoding/json" "errors" - "fmt" + "os" + "path" "testing" "time" "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,18 +38,9 @@ import ( ) const ( - DeploymentTemplateTestWaitDuration = time.Second * 10 - DeploymentTemplateTestWaitInterval = time.Second * 1 - DeploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 - - TestDeploymentTemplateNamespace = "basic" - TestDeploymentTemplateName = "test-deploymenttemplate" - TestDeploymentTemplateRadiusResourceGroup = "default-basic" -) - -var ( - TestDeploymentTemplateScope = fmt.Sprintf("/planes/radius/local/resourcegroups/%s", TestDeploymentTemplateRadiusResourceGroup) - TestDeploymentTemplateID = fmt.Sprintf("%s/providers/Microsoft.Resources/deployments/%s", TestDeploymentTemplateScope, TestDeploymentTemplateName) + deploymentTemplateTestWaitDuration = time.Second * 10 + deploymentTemplateTestWaitInterval = time.Second * 1 + deploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 ) func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client) { @@ -69,12 +63,24 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client require.NoError(t, err) radius := NewMockRadiusClient() + + // Set up DeploymentTemplateReconciler. err = (&DeploymentTemplateReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("controller"), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: radius, + DelayInterval: deploymentTemplateTestControllerDelayInterval, + }).SetupWithManager(mgr) + require.NoError(t, err) + + // Set up DeploymentResourceReconciler. + err = (&DeploymentResourceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), Radius: radius, - DelayInterval: DeploymentTemplateTestControllerDelayInterval, + DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) @@ -90,62 +96,64 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) radius, client := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "basic", Name: "test-deploymenttemplate"} + name := types.NamespacedName{Namespace: "deploymenttemplate-basic", Name: "test-deploymenttemplate-basic"} err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - deployment := makeDeploymentTemplate(name, map[string]any{}) - err = client.Create(ctx, deployment) + deploymentTemplate := makeDeploymentTemplate(name, "{}", generateDefaultProviderConfig(), "deploymenttemplate-basic.bicep", map[string]string{}) + err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) - // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") - - // Deployment will be waiting for template to complete provisioning. + // Wait for the DeploymentTemplate to enter the updating state. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + // Verify the provider config is parsed correctly. scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default-basic", scope) + require.Equal(t, "/planes/radius/local/resourcegroups/default", scope) radius.CompleteOperation(status.Operation.ResumeToken, nil) - // Deployment will update after operation completes + // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/default-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate", status.Resource) - - resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) + require.Equal(t, "/planes/radius/local/resourcegroups/default/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) + // Verify that the Radius deployment contains the expected properties. expectedProperties := map[string]any{ "mode": "Incremental", - "parameters": map[string]map[string]any{}, - "providerConfig": map[string]any{ - "deployments": map[string]any{ - "type": "Microsoft.Resources", - "value": map[string]any{ - "scope": "/planes/radius/local/resourcegroups/default-basic", + "template": map[string]any{}, + "parameters": map[string]map[string]string{}, + "providerConfig": sdkclients.ProviderConfig{ + Radius: &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/default", }, }, - "radius": map[string]any{ - "type": "Radius", - "value": map[string]any{ - "scope": "/planes/radius/local/resourcegroups/default-basic", + Deployments: &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/default", }, }, - }, "template": map[string]any{}, + }, } + resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) require.Equal(t, expectedProperties, resource.Properties) - err = client.Delete(ctx, deployment) - require.NoError(t, err) + // Verify that the DeploymentTemplate contains the expected properties. + require.Equal(t, "{}", status.Template) + require.Equal(t, "{}", status.Parameters) + require.Equal(t, string(generateDefaultProviderConfig()), status.ProviderConfig) + require.Equal(t, "deploymenttemplate-basic.bicep", status.RootFileName) - // Deletion of the DeploymentTemplate is in progress. - status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) - radius.CompleteOperation(status.Operation.ResumeToken, nil) + // Delete the DeploymentTemplate + err = client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) - // Now deleting of the DeploymentTemplate object can complete. - waitForDeploymentTemplateDeleted(t, client, name) + // Wait for the DeploymentTemplate to be deleted. + waitForDeploymentTemplateStateDeleted(t, client, name) } func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { @@ -157,24 +165,21 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { ctx := testcontext.New(t) radius, client := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "failure-recovery", Name: "test-failure-recovery"} + name := types.NamespacedName{Namespace: "deploymenttemplate-failurerecovery", Name: "test-deploymenttemplate-failurerecovery"} err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - deployment := makeDeploymentTemplate(name, map[string]any{}) - err = client.Create(ctx, deployment) + deploymentTemplate := makeDeploymentTemplate(name, "{}", generateDefaultProviderConfig(), "deploymenttemplate-failurerecovery.bicep", map[string]string{}) + err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) - // Deployment will be waiting for environment to be created. - createEnvironment(radius, "default") - - // Deployment will be waiting for template to complete provisioning. + // Wait for the DeploymentTemplate to enter the updating state. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) // Complete the operation, but make it fail. operation := status.Operation radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - state.err = errors.New("oops") + state.err = errors.New("failure") resource, ok := radius.resources[state.resourceID] require.True(t, ok, "failed to find resource") @@ -183,38 +188,115 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) - // Deployment should (eventually) start a new provisioning operation + // DeploymentTemplate should (eventually) start a new provisioning operation status = waitForDeploymentTemplateStateUpdating(t, client, name, operation) // Complete the operation, successfully this time. radius.CompleteOperation(status.Operation.ResumeToken, nil) _ = waitForDeploymentTemplateStateReady(t, client, name) - err = client.Delete(ctx, deployment) + err = client.Delete(ctx, deploymentTemplate) require.NoError(t, err) - // Deletion of the deployment is in progress. - status = waitForDeploymentTemplateStateDeleting(t, client, name, nil) + waitForDeploymentTemplateStateDeleted(t, client, name) +} - // Complete the operation, but make it fail. - operation = status.Operation - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - state.err = errors.New("oops") +func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { + ctx := testcontext.New(t) + radius, client := SetupDeploymentTemplateTest(t) + + name := types.NamespacedName{Namespace: "deploymenttemplate-withresources", Name: "test-deploymenttemplate-withresources"} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-withresources.json")) + require.NoError(t, err) + templateMap := map[string]any{} + err = json.Unmarshal(fileContent, &templateMap) + require.NoError(t, err) + template, err := json.MarshalIndent(templateMap, "", " ") + require.NoError(t, err) + + deploymentTemplate := makeDeploymentTemplate(name, string(template), generateDefaultProviderConfig(), "deploymenttemplate-withresources.bicep", map[string]string{}) + err = client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + + status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + // Verify the provider config is parsed correctly. + scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) + require.NoError(t, err) + require.Equal(t, "/planes/radius/local/resourcegroups/default", scope) + + radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { resource, ok := radius.resources[state.resourceID] require.True(t, ok, "failed to find resource") - resource.Properties["provisioningState"] = "Failed" + resource.Properties["outputResources"] = []any{ + map[string]any{"id": "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env"}, + } + state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) - // Deployment should (eventually) start a new deletion operation - status = waitForDeploymentTemplateStateDeleting(t, client, name, operation) + // DeploymentTemplate should be ready after the operation completes. + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/default/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) - // Complete the operation, successfully this time. - radius.CompleteOperation(status.Operation.ResumeToken, nil) + // DeploymentTemplate will be waiting for environment to be created. + createEnvironment(radius, "default", "env") + + dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "env"} + dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) + require.Equal(t, "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env", dependencyStatus.Id) - // Now deleting of the deployment object can complete. - waitForDeploymentTemplateDeleted(t, client, name) + // Verify that the Radius deployment contains the expected properties. + resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) + expectedProperties := map[string]any{ + "mode": "Incremental", + "template": templateMap, + "parameters": map[string]map[string]string{}, + "providerConfig": sdkclients.ProviderConfig{ + Radius: &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/default", + }, + }, + Deployments: &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/default", + }, + }, + }, + "outputResources": []any{ + map[string]any{"id": "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env"}, + }, + } + require.Equal(t, expectedProperties, resource.Properties) + + // Verify that the DeploymentTemplate contains the expected properties. + require.Equal(t, string(template), status.Template) + require.Equal(t, "{}", status.Parameters) + require.Equal(t, string(generateDefaultProviderConfig()), status.ProviderConfig) + require.Equal(t, "deploymenttemplate-withresources.bicep", status.RootFileName) + + err = client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) + + waitForDeploymentTemplateStateDeleting(t, client, name, nil) + + dependencyStatus = waitForDeploymentResourceStateDeleting(t, client, dependencyName, nil) + + // Delete the environment. + deleteEnvironment(radius, "default", "env") + + // Complete the delete operation on the DeploymentResource. + radius.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) + + waitForDeploymentResourceDeleted(t, client, dependencyName) + waitForDeploymentTemplateStateDeleted(t, client, name) } func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { @@ -224,7 +306,11 @@ func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, status := &radappiov1alpha3.DeploymentTemplateStatus{} require.EventuallyWithT(t, func(t *assert.CollectT) { logger.Logf("Fetching DeploymentTemplate: %+v", name) - current := &radappiov1alpha3.DeploymentTemplate{} + current := &radappiov1alpha3.DeploymentTemplate{ + Status: radappiov1alpha3.DeploymentTemplateStatus{ + Phrase: radappiov1alpha3.DeploymentTemplatePhrase(radappiov1alpha3.DeploymentResourcePhraseDeleting), + }, + } err := client.Get(ctx, name, current) require.NoError(t, err) @@ -237,7 +323,7 @@ func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, assert.NotEqual(t, oldOperation, current.Status.Operation) } - }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter updating state") + }, deploymentTemplateTestWaitDuration, deploymentTemplateTestWaitInterval, "failed to enter updating state") return status } @@ -260,7 +346,7 @@ func waitForDeploymentTemplateStateReady(t *testing.T, client client.Client, nam if assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseReady, current.Status.Phrase) { assert.Empty(t, current.Status.Operation) } - }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter updating state") + }, deploymentTemplateTestWaitDuration, deploymentTemplateTestWaitInterval, "failed to enter ready state") return status } @@ -280,16 +366,13 @@ func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, logger.Logf("DeploymentTemplate.Status: %+v", current.Status) assert.Equal(t, status.ObservedGeneration, current.Generation, "Status is not updated") - if assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseDeleting, current.Status.Phrase) { - assert.NotEmpty(t, current.Status.Operation) - assert.NotEqual(t, oldOperation, current.Status.Operation) - } - }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "failed to enter deleting state") + assert.Equal(t, radappiov1alpha3.DeploymentTemplatePhraseDeleting, current.Status.Phrase) + }, deploymentTemplateTestWaitDuration, deploymentTemplateTestWaitInterval, "failed to enter deleting state") return status } -func waitForDeploymentTemplateDeleted(t *testing.T, client client.Client, name types.NamespacedName) { +func waitForDeploymentTemplateStateDeleted(t *testing.T, client client.Client, name types.NamespacedName) { ctx := testcontext.New(t) logger := t @@ -304,5 +387,5 @@ func waitForDeploymentTemplateDeleted(t *testing.T, client client.Client, name t logger.Logf("DeploymentTemplate.Status: %+v", current.Status) return false - }, DeploymentTemplateTestWaitDuration, DeploymentTemplateTestWaitInterval, "DeploymentTemplate still exists") + }, deploymentTemplateTestWaitDuration, deploymentTemplateTestWaitInterval, "DeploymentTemplate still exists") } diff --git a/pkg/controller/reconciler/mock_client_test.go b/pkg/controller/reconciler/mock_client_test.go index 08bd96c2e8..d5eb8b5008 100644 --- a/pkg/controller/reconciler/mock_client_test.go +++ b/pkg/controller/reconciler/mock_client_test.go @@ -75,6 +75,13 @@ func (rc *mockRadiusClient) Update(exec func()) { exec() } +func (rc *mockRadiusClient) Delete(exec func()) { + rc.lock.Lock() + defer rc.lock.Unlock() + + exec() +} + func (rc *mockRadiusClient) Applications(scope string) ApplicationClient { return &mockApplicationClient{mock: rc, scope: scope} } diff --git a/pkg/controller/reconciler/recipe_reconciler_test.go b/pkg/controller/reconciler/recipe_reconciler_test.go index d403829a45..ed2a91bf5d 100644 --- a/pkg/controller/reconciler/recipe_reconciler_test.go +++ b/pkg/controller/reconciler/recipe_reconciler_test.go @@ -90,7 +90,7 @@ func Test_RecipeReconciler_WithoutSecret(t *testing.T) { require.NoError(t, err) // Recipe will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Recipe will be waiting for extender to complete provisioning. status := waitForRecipeStateUpdating(t, client, name, nil) @@ -132,7 +132,7 @@ func Test_RecipeReconciler_ChangeEnvironmentAndApplication(t *testing.T) { require.NoError(t, err) // Recipe will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Recipe will be waiting for extender to complete provisioning. status := waitForRecipeStateUpdating(t, client, name, nil) @@ -146,7 +146,7 @@ func Test_RecipeReconciler_ChangeEnvironmentAndApplication(t *testing.T) { status = waitForRecipeStateReady(t, client, name) require.Equal(t, "/planes/radius/local/resourcegroups/default-recipe-change-envapp/providers/Applications.Core/extenders/test-recipe-change-envapp", status.Resource) - createEnvironment(radius, "new-environment") + createEnvironment(radius, "new-environment", "new-environment") // Now update the recipe to change the environment and application. err = client.Get(ctx, name, recipe) @@ -209,7 +209,7 @@ func Test_RecipeReconciler_FailureRecovery(t *testing.T) { require.NoError(t, err) // Recipe will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Recipe will be waiting for extender to complete provisioning. status := waitForRecipeStateUpdating(t, client, name, nil) @@ -275,7 +275,7 @@ func Test_RecipeReconciler_WithSecret(t *testing.T) { require.NoError(t, err) // Recipe will be waiting for environment to be created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") // Recipe will be waiting for extender to complete provisioning. status := waitForRecipeStateUpdating(t, client, name, nil) diff --git a/pkg/controller/reconciler/recipe_webhook_test.go b/pkg/controller/reconciler/recipe_webhook_test.go index 7882cafb2b..8bb2c0c857 100644 --- a/pkg/controller/reconciler/recipe_webhook_test.go +++ b/pkg/controller/reconciler/recipe_webhook_test.go @@ -54,7 +54,7 @@ func Test_ValidateRecipe_Type(t *testing.T) { radius, client := setupWebhookTest(t) // Environment is created. - createEnvironment(radius, "default") + createEnvironment(radius, "default", "default") t.Run("test recipe for invalid type", func(t *testing.T) { recipeName := "test-recipe-invalidtype" diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index c5a44ef850..4c83fc9a37 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -17,7 +17,6 @@ limitations under the License. package reconciler import ( - "encoding/json" "fmt" "testing" "time" @@ -44,8 +43,8 @@ const ( recipeTestControllerDelayInterval = time.Millisecond * 100 ) -func createEnvironment(radius *mockRadiusClient, name string) { - id := fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", name, name) +func createEnvironment(radius *mockRadiusClient, resourceGroup, name string) { + id := fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", resourceGroup, name) radius.Update(func() { radius.environments[id] = v20231001preview.EnvironmentResource{ ID: to.Ptr(id), @@ -55,6 +54,13 @@ func createEnvironment(radius *mockRadiusClient, name string) { }) } +func deleteEnvironment(radius *mockRadiusClient, resourceGroup, name string) { + id := fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", resourceGroup, name) + radius.Delete(func() { + delete(radius.environments, id) + }) +} + func makeRecipe(name types.NamespacedName, resourceType string) *radappiov1alpha3.Recipe { return &radappiov1alpha3.Recipe{ ObjectMeta: ctrl.ObjectMeta{ @@ -193,19 +199,17 @@ func boolPtr(b bool) *bool { return &b } -func makeDeploymentTemplate(name types.NamespacedName, template map[string]any) *radappiov1alpha3.DeploymentTemplate { - b, err := json.Marshal(template) - if err != nil { - panic(err) - } - +func makeDeploymentTemplate(name types.NamespacedName, template string, providerConfig string, rootFileName string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { return &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: ctrl.ObjectMeta{ Namespace: name.Namespace, Name: name.Name, }, Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: string(b), + Template: template, + ProviderConfig: providerConfig, + RootFileName: rootFileName, + Parameters: parameters, }, } } @@ -221,3 +225,22 @@ func makeDeploymentResource(name types.NamespacedName, id string) *radappiov1alp }, } } + +func generateDefaultProviderConfig() string { + return ` + { + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourcegroups/default" + } + }, + "radius": { + "type": "Radius", + "value": { + "scope": "/planes/radius/local/resourcegroups/default" + } + } + } + ` +} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json new file mode 100644 index 0000000000..910cb2fa0c --- /dev/null +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "17470211592317605856" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "env", + "location": "global", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "default" + } + } + } + } + } +} \ No newline at end of file From 8fdfc6c44507eeddfae0634c433242a3509b48d0 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 6 Dec 2024 10:01:34 -0800 Subject: [PATCH 32/65] Fixing tests Signed-off-by: willdavsmith --- .../reconciler/deployment_reconciler_test.go | 2 +- .../reconciler/deploymentresource_reconciler_test.go | 10 ++++++++++ .../reconciler/deploymenttemplate_reconciler_test.go | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pkg/controller/reconciler/deployment_reconciler_test.go b/pkg/controller/reconciler/deployment_reconciler_test.go index e60f7e332d..6b5ea42b30 100644 --- a/pkg/controller/reconciler/deployment_reconciler_test.go +++ b/pkg/controller/reconciler/deployment_reconciler_test.go @@ -167,7 +167,7 @@ func Test_DeploymentReconciler_ChangeEnvironmentAndApplication(t *testing.T) { annotations = waitForStateReady(t, client, name) require.Equal(t, "/planes/radius/local/resourcegroups/default-deployment-change-envapp/providers/Applications.Core/containers/test-deployment-change-envapp", annotations.Status.Container) - createEnvironment(radius, "new-environment", "default") + createEnvironment(radius, "new-environment", "new-environment") // Now update the deployment to change the environment and application. err = client.Get(ctx, name, deployment) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index 3c52bbeb4d..36b29faf8c 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -30,6 +30,8 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + crconfig "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) const ( @@ -63,6 +65,14 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, + Controller: crconfig.Controller{ + SkipNameValidation: boolPtr(true), + }, + + // Suppress metrics in tests to avoid conflicts. + Metrics: server.Options{ + BindAddress: "0", + }, }) require.NoError(t, err) diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 240c3539b7..f235c9ff00 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -35,6 +35,8 @@ import ( "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + crconfig "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) const ( @@ -59,6 +61,14 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, + Controller: crconfig.Controller{ + SkipNameValidation: boolPtr(true), + }, + + // Suppress metrics in tests to avoid conflicts. + Metrics: server.Options{ + BindAddress: "0", + }, }) require.NoError(t, err) From d6a6c428feeff1704f6151ac3ba913a75e6303c8 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 23 Dec 2024 14:43:26 -0800 Subject: [PATCH 33/65] PR Signed-off-by: willdavsmith --- deploy/Chart/templates/controller/rbac.yaml | 14 - pkg/cli/bicep/deployment_parameters.go | 9 +- pkg/cli/bicep/deployment_parameters_test.go | 34 +- .../generatekubernetesmanifest.go | 80 +-- .../generatekubernetesmanifest_test.go | 634 ++---------------- .../testdata/aws/aws.bicep | 11 - .../testdata/aws/aws.yaml | 74 -- .../testdata/azure/azure.bicep | 9 - .../testdata/azure/azure.yaml | 59 -- .../testdata/basic/basic.bicep | 20 - .../testdata/basic/basic.yaml | 70 -- .../deploymenttemplate-parameters.json | 5 + .../deploymenttemplate.bicep} | 2 +- .../deploymenttemplate.json | 56 ++ .../deploymenttemplate.yaml} | 13 +- .../testdata/module/module.bicep | 14 - .../testdata/module/module.yaml | 119 ---- .../testdata/module/storage.bicep | 14 - .../testdata/parameters.json | 2 +- .../testdata/parameters/parameters.json | 9 - pkg/cli/cmd/deploy/deploy.go | 4 +- pkg/cli/cmd/recipe/register/register.go | 5 +- pkg/cli/filesystem/filesystem.go | 15 + pkg/cli/filesystem/memmapfs.go | 52 ++ pkg/cli/filesystem/memmapfs_test.go | 83 +++ pkg/cli/filesystem/osfs.go | 41 ++ pkg/controller/reconciler/const.go | 3 - .../deploymentresource_reconciler.go | 128 ++-- .../deploymenttemplate_reconciler.go | 14 +- .../deploymenttemplate-withresources.json | 6 +- pkg/controller/reconciler/util.go | 9 +- pkg/controller/reconciler/util_test.go | 88 +++ .../noncloud/deploymenttemplate_test.go | 295 +++++--- .../noncloud/testdata/env/env.bicep | 2 +- .../kubernetes/noncloud/testdata/env/env.json | 12 +- .../testdata/module/module-dependency.bicep | 14 +- .../noncloud/testdata/module/module.bicep | 18 +- .../noncloud/testdata/module/module.json | 71 +- .../noncloud/testdata/recipe/recipe.bicep | 40 ++ .../noncloud/testdata/recipe/recipe.json | 88 +++ 40 files changed, 987 insertions(+), 1249 deletions(-) delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate-parameters.json rename pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/{parameters/parameters.bicep => deploymenttemplate/deploymenttemplate.bicep} (84%) create mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json rename pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/{parameters/parameters.yaml => deploymenttemplate/deploymenttemplate.yaml} (88%) delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep delete mode 100644 pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json create mode 100644 pkg/cli/filesystem/filesystem.go create mode 100644 pkg/cli/filesystem/memmapfs.go create mode 100644 pkg/cli/filesystem/memmapfs_test.go create mode 100644 pkg/cli/filesystem/osfs.go create mode 100644 pkg/controller/reconciler/util_test.go create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json diff --git a/deploy/Chart/templates/controller/rbac.yaml b/deploy/Chart/templates/controller/rbac.yaml index 0a6870ad13..db554aa8d5 100644 --- a/deploy/Chart/templates/controller/rbac.yaml +++ b/deploy/Chart/templates/controller/rbac.yaml @@ -56,20 +56,6 @@ rules: - '*' verbs: - '*' -- apiGroups: - - source.toolkit.fluxcd.io - resources: - - gitrepositories - verbs: - - get - - list - - watch -- apiGroups: - - source.toolkit.fluxcd.io - resources: - - gitrepositories/status - verbs: - - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/pkg/cli/bicep/deployment_parameters.go b/pkg/cli/bicep/deployment_parameters.go index de37cede94..a88275be13 100644 --- a/pkg/cli/bicep/deployment_parameters.go +++ b/pkg/cli/bicep/deployment_parameters.go @@ -21,15 +21,14 @@ import ( "fmt" "strings" - "github.com/spf13/afero" - "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/filesystem" ) // ParameterParser is used to parse the parameters as part of the `rad deploy` command. See the docs for `rad deploy` for examples // of what we need to support here. type ParameterParser struct { - FileSystem afero.Fs + FileSystem filesystem.FileSystem } type ParameterFile struct { @@ -81,7 +80,7 @@ func (pp ParameterParser) parseSingle(input string, output clients.DeploymentPar if strings.HasPrefix(input, "@") { // input is a file that declares multiple parameters filePath := strings.TrimPrefix(input, "@") - b, err := afero.ReadFile(pp.FileSystem, filePath) + b, err := pp.FileSystem.ReadFile(filePath) if err != nil { return err } @@ -102,7 +101,7 @@ func (pp ParameterParser) parseSingle(input string, output clients.DeploymentPar if strings.HasPrefix(parameterValue, "@") { // input is a file that declares a single parameter filePath := strings.TrimPrefix(parameterValue, "@") - b, err := afero.ReadFile(pp.FileSystem, filePath) + b, err := pp.FileSystem.ReadFile(filePath) if err != nil { return err } diff --git a/pkg/cli/bicep/deployment_parameters_test.go b/pkg/cli/bicep/deployment_parameters_test.go index e355aa13f9..25770b2dc7 100644 --- a/pkg/cli/bicep/deployment_parameters_test.go +++ b/pkg/cli/bicep/deployment_parameters_test.go @@ -21,10 +21,10 @@ import ( "os" "path/filepath" "testing" - - "github.com/spf13/afero" + "testing/fstest" "github.com/radius-project/radius/pkg/cli/clients" + "github.com/radius-project/radius/pkg/cli/filesystem" "github.com/stretchr/testify/require" ) @@ -37,7 +37,7 @@ func Test_Parameters_Invalid(t *testing.T) { } parser := ParameterParser{ - FileSystem: afero.NewMemMapFs(), + FileSystem: filesystem.NewMemMapFileSystem(), } for _, input := range inputs { @@ -57,24 +57,18 @@ func Test_ParseParameters_Overwrite(t *testing.T) { "key3=value3", } - // Initialize the in-memory filesystem - fs := afero.NewMemMapFs() - - // Create the "many.json" file with the specified content - err := afero.WriteFile(fs, "many.json", []byte(`{ "parameters": { "key1": { "value": { "someValue": true } }, "key2": { "value": "overridden-value" } } }`), 0644) - if err != nil { - t.Fatalf("Failed to create many.json: %v", err) - } - - // Create the "single.json" file with the specified content - err = afero.WriteFile(fs, "single.json", []byte(`{ "someValue": "another-value" }`), 0644) - if err != nil { - t.Fatalf("Failed to create single.json: %v", err) - } - // Initialize the ParameterParser with the in-memory filesystem parser := ParameterParser{ - FileSystem: fs, + FileSystem: filesystem.MemMapFileSystem{ + InternalFileSystem: fstest.MapFS{ + "many.json": { + Data: []byte(`{ "parameters": { "key1": { "value": { "someValue": true } }, "key2": { "value": "overridden-value" } } }`), + }, + "single.json": { + Data: []byte(`{ "someValue": "another-value" }`), + }, + }, + }, } parameters, err := parser.Parse(inputs...) @@ -101,7 +95,7 @@ func Test_ParseParameters_Overwrite(t *testing.T) { func Test_ParseParameters_File(t *testing.T) { parser := ParameterParser{ - FileSystem: afero.NewMemMapFs(), + FileSystem: filesystem.NewMemMapFileSystem(), } input, err := os.ReadFile(filepath.Join("testdata", "test-parameters.json")) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index ceb1ebbfec..eb0c7f65cc 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -23,17 +23,21 @@ import ( "path/filepath" "strings" - "github.com/spf13/afero" - "github.com/radius-project/radius/pkg/cli/bicep" + "github.com/radius-project/radius/pkg/cli/clierrors" "github.com/radius-project/radius/pkg/cli/cmd/commonflags" "github.com/radius-project/radius/pkg/cli/connections" "github.com/radius-project/radius/pkg/cli/deploy" + "github.com/radius-project/radius/pkg/cli/filesystem" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" +) + +const ( + resourceGroupRequiredMessage = "ResourceGroup is required. Please provide a value for the --resource-group flag." ) // NewCommand creates a command for the `rad bicep generate-kubernetes-manifest` command. @@ -62,23 +66,20 @@ func NewCommand(factory framework.Factory) (*cobra.Command, framework.Runner) { `, Example: ` # Generate a DeploymentTemplate Custom Resource from a Bicep file. -rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam --parameters tag=latest --outfile app.yaml +rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam --parameters tag=latest --destination-file app.yaml --resource-group default `, Args: cobra.ExactArgs(1), RunE: framework.RunCommand(runner), } - commonflags.AddWorkspaceFlag(cmd) commonflags.AddResourceGroupFlag(cmd) - commonflags.AddEnvironmentNameFlag(cmd) commonflags.AddParameterFlag(cmd) - cmd.Flags().String("outfile", "", "Path of the generated DeploymentTemplate yaml file.") - _ = cmd.MarkFlagFilename("outfile", ".yaml") + cmd.Flags().StringP("destination-file", "d", "", "Path of the generated DeploymentTemplate yaml file.") + _ = cmd.MarkFlagFilename("destination-file", ".yaml") cmd.Flags().String("azure-scope", "", "Scope for Azure deployment.") cmd.Flags().String("aws-scope", "", "Scope for AWS deployment.") - cmd.Flags().String("deployment-scope", "", "Scope for the Radius deployment.") return cmd, runner } @@ -91,11 +92,11 @@ type Runner struct { Deploy deploy.Interface Output output.Interface - FileSystem afero.Fs + FileSystem filesystem.FileSystem + Group string FilePath string Parameters map[string]map[string]any - OutFile string - DeploymentScope string + DestinationFile string AzureScope string AWSScope string } @@ -116,13 +117,13 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { r.FilePath = args[0] var err error - r.DeploymentScope, err = cmd.Flags().GetString("deployment-scope") + r.Group, err = cmd.Flags().GetString("group") if err != nil { return err } - if r.DeploymentScope == "" { - r.DeploymentScope = "/planes/radius/local/resourceGroups/default" + if r.Group == "" { + return clierrors.Message(resourceGroupRequiredMessage) } r.AzureScope, err = cmd.Flags().GetString("azure-scope") @@ -135,18 +136,27 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } - r.OutFile, err = cmd.Flags().GetString("outfile") + r.DestinationFile, err = cmd.Flags().GetString("destination-file") if err != nil { return err } + // If the destination file is not provided, use the base name of the file with a .yaml extension + if r.DestinationFile == "" { + r.DestinationFile = strings.TrimSuffix(filepath.Base(r.FilePath), filepath.Ext(r.FilePath)) + ".yaml" + } + + if filepath.Ext(r.DestinationFile) != ".yaml" { + return clierrors.Message("Destination file must have a .yaml extension") + } + parameterArgs, err := cmd.Flags().GetStringArray("parameters") if err != nil { return err } if r.FileSystem == nil { - r.FileSystem = afero.NewOsFs() + r.FileSystem = filesystem.NewOSFS() } parser := bicep.ParameterParser{FileSystem: r.FileSystem} @@ -165,12 +175,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } - // create a DeploymentTemplate yaml file - // with the basefilename from the bicepfile - if r.OutFile == "" { - r.OutFile = strings.TrimSuffix(filepath.Base(r.FilePath), filepath.Ext(r.FilePath)) + ".yaml" - } - deploymentTemplate, err := r.generateDeploymentTemplate(filepath.Base(r.FilePath), template, r.Parameters) if err != nil { return err @@ -182,7 +186,7 @@ func (r *Runner) Run(ctx context.Context) error { } // Print the path to the file - r.Output.LogInfo("DeploymentTemplate file created at %s", r.OutFile) + r.Output.LogInfo("DeploymentTemplate file created at %s", r.DestinationFile) return nil } @@ -210,8 +214,7 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string "kind": "DeploymentTemplate", "apiVersion": "radapp.io/v1alpha3", "metadata": map[string]any{ - "name": fileName, - "namespace": "radius-system", + "name": fileName, }, "spec": map[string]any{ "template": string(marshalledTemplate), @@ -226,25 +229,12 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string // createDeploymentTemplateYAMLFile creates a DeploymentTemplate YAML file with the given content. func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string]any) error { - fmt.Println("Creating DeploymentTemplate YAML file") - f, err := r.FileSystem.Create(r.OutFile) - if err != nil { - return err - } - - defer f.Close() - deploymentTemplateYaml, err := yaml.Marshal(deploymentTemplate) if err != nil { return err } - _, err = f.Write(deploymentTemplateYaml) - if err != nil { - return err - } - - return nil + return r.FileSystem.WriteFile(r.DestinationFile, deploymentTemplateYaml, 0644) } // generateProviderConfig generates a ProviderConfig object based on the given scopes. @@ -266,20 +256,24 @@ func (r *Runner) generateProviderConfig() (providerConfig sdkclients.ProviderCon }, } } - if r.DeploymentScope != "" { + if r.Group != "" { providerConfig.Radius = &sdkclients.Radius{ Type: "radius", Value: sdkclients.Value{ - Scope: r.DeploymentScope, + Scope: constructRadiusDeploymentScope(r.Group), }, } providerConfig.Deployments = &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: r.DeploymentScope, + Scope: constructRadiusDeploymentScope(r.Group), }, } } return providerConfig } + +func constructRadiusDeploymentScope(group string) string { + return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go index 629bb4895d..5d8e8d14af 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go @@ -19,15 +19,16 @@ package bicep import ( "context" "encoding/json" + "fmt" "os" "path/filepath" "testing" "github.com/radius-project/radius/pkg/cli/bicep" + "github.com/radius-project/radius/pkg/cli/filesystem" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/test/radcli" - "github.com/spf13/afero" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -38,136 +39,92 @@ func Test_CommandValidation(t *testing.T) { } func Test_Validate(t *testing.T) { - configWithWorkspace := radcli.LoadConfigWithWorkspace(t) testcases := []radcli.ValidateInput{ { - Name: "rad bicep generate-kubernetes-manifest - valid", - Input: []string{"app.bicep"}, + Name: "rad bicep generate-kubernetes-manifest - valid with group short flag", + Input: []string{"app.bicep", "-g", "default"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, ValidateCallback: func(t *testing.T, r framework.Runner) { runner := r.(*Runner) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) + require.Equal(t, "default", runner.Group) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with parameters", - Input: []string{"app.bicep", "-p", "foo=bar", "--parameters", "a=b"}, + Name: "rad bicep generate-kubernetes-manifest - valid with group long flag", + Input: []string{"app.bicep", "--group", "default"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, ValidateCallback: func(t *testing.T, r framework.Runner) { runner := r.(*Runner) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) + require.Equal(t, "default", runner.Group) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with parameters file", - Input: []string{"app.bicep", "--parameters", "@testdata/parameters.json"}, + Name: "rad bicep generate-kubernetes-manifest - valid with parameters", + Input: []string{"app.bicep", "-g", "default", "-p", "foo=bar", "--parameters", "a=b", "--parameters", "@testdata/parameters.json"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, ValidateCallback: func(t *testing.T, r framework.Runner) { runner := r.(*Runner) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) + expectedParameters := map[string]map[string]any{ + "foo": { + "value": "bar", + }, + "a": { + "value": "b", + }, + "b": { + "value": "c", + }, + } + require.Equal(t, expectedParameters, runner.Parameters) }, }, { Name: "rad bicep generate-kubernetes-manifest - invalid parameter format", - Input: []string{"app.bicep", "--parameters", "invalid-format"}, + Input: []string{"app.bicep", "-g", "default", "--parameters", "invalid-format"}, ExpectedValid: false, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), - }, }, { - Name: "rad bicep generate-kubernetes-manifest - too many args", - Input: []string{"app.bicep", "anotherfile.bicep"}, + Name: "rad bicep generate-kubernetes-manifest - missing file argument", + Input: []string{}, ExpectedValid: false, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), - }, - }, - { - Name: "rad bicep generate-kubernetes-manifest - valid with outfile", - Input: []string{"app.bicep", "--outfile", "test.yaml"}, - ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, - ValidateCallback: func(t *testing.T, r framework.Runner) { - runner := r.(*Runner) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) - }, }, { - Name: "rad bicep generate-kubernetes-manifest - invalid outfile", - Input: []string{"app.bicep", "test.json"}, + Name: "rad bicep generate-kubernetes-manifest - too many args", + Input: []string{"app.bicep", "-g", "default", "anotherfile.bicep"}, ExpectedValid: false, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), - }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with azure scope", - Input: []string{"app.bicep", "--azure-scope", "azure-scope-value"}, + Name: "rad bicep generate-kubernetes-manifest - valid with destination file long flag", + Input: []string{"app.bicep", "-g", "default", "--destination-file", "test.yaml"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, ValidateCallback: func(t *testing.T, r framework.Runner) { runner := r.(*Runner) - require.Equal(t, "azure-scope-value", runner.AzureScope) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) + require.Equal(t, "test.yaml", runner.DestinationFile) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with aws scope", - Input: []string{"app.bicep", "--aws-scope", "aws-scope-value"}, + Name: "rad bicep generate-kubernetes-manifest - valid with destination file short flag", + Input: []string{"app.bicep", "-g", "default", "-d", "test.yaml"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, ValidateCallback: func(t *testing.T, r framework.Runner) { runner := r.(*Runner) - require.Equal(t, "aws-scope-value", runner.AWSScope) - require.Equal(t, "/planes/radius/local/resourceGroups/default", runner.DeploymentScope) + require.Equal(t, "test.yaml", runner.DestinationFile) }, }, { - Name: "rad bicep generate-kubernetes-manifest - valid with deployment scope", - Input: []string{"app.bicep", "--deployment-scope", "deployment-scope-value"}, + Name: "rad bicep generate-kubernetes-manifest - invalid destination file", + Input: []string{"app.bicep", "-g", "default", "--destination-file", "test.json"}, + ExpectedValid: false, + }, + { + Name: "rad bicep generate-kubernetes-manifest - valid with azure scope", + Input: []string{"app.bicep", "-g", "default", "--azure-scope", "azure-scope-value"}, ExpectedValid: true, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: configWithWorkspace, - }, - ValidateCallback: func(t *testing.T, r framework.Runner) { - runner := r.(*Runner) - require.Equal(t, "deployment-scope-value", runner.DeploymentScope) - }, }, { - Name: "rad bicep generate-kubernetes-manifest - missing file argument", - Input: []string{}, - ExpectedValid: false, - ConfigHolder: framework.ConfigHolder{ - ConfigFilePath: "", - Config: radcli.LoadEmptyConfig(t), - }, + Name: "rad bicep generate-kubernetes-manifest - valid with aws scope", + Input: []string{"app.bicep", "-g", "default", "--aws-scope", "aws-scope-value"}, + ExpectedValid: true, }, } @@ -175,524 +132,65 @@ func Test_Validate(t *testing.T) { } func Test_Run(t *testing.T) { - t.Run("Create DeploymentTemplate (basic)", func(t *testing.T) { + t.Run("Create DeploymentTemplate", func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - template := ` - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "10886769892319697000", - "version": "0.30.23.60470" - } - }, - "resources": { - "basic": { - "import": "Radius", - "properties": { - "name": "basic", - "properties": { - "compute": { - "kind": "kubernetes", - "namespace": "default", - "resourceId": "self" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" - } - } - } - } - }, - "type": "Applications.Core/environments@2023-10-01-preview" - } - } - } - ` + resourceGroup := "default" + testName := "deploymenttemplate" + bicepFilePath := fmt.Sprintf("%s.bicep", testName) + parametersFilePath := fmt.Sprintf("%s-parameters.json", testName) + jsonFilePath := fmt.Sprintf("%s.json", testName) + yamlFilePath := fmt.Sprintf("%s.yaml", testName) - var templateMap map[string]any - err := json.Unmarshal([]byte(template), &templateMap) + template, err := os.ReadFile(filepath.Join("testdata", testName, jsonFilePath)) require.NoError(t, err) - bicep := bicep.NewMockInterface(ctrl) - bicep.EXPECT(). - PrepareTemplate("basic.bicep"). - Return(templateMap, nil). - Times(1) - - filePath := "basic.bicep" - - outputSink := &output.MockOutput{} - runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - FileSystem: afero.NewMemMapFs(), - DeploymentScope: "/planes/radius/local/resourceGroups/default", - } - - fileExists, err := afero.Exists(runner.FileSystem, "basic.yaml") - require.NoError(t, err) - require.False(t, fileExists) - - err = runner.Run(context.Background()) - require.NoError(t, err) - - fileExists, err = afero.Exists(runner.FileSystem, "basic.yaml") - require.NoError(t, err) - require.True(t, fileExists) - - require.Equal(t, "basic.yaml", runner.OutFile) - - expected, err := os.ReadFile(filepath.Join("testdata", "basic", "basic.yaml")) - require.NoError(t, err) - - actual, err := afero.ReadFile(runner.FileSystem, "basic.yaml") - require.NoError(t, err) - require.Equal(t, string(expected), string(actual)) - }) - - t.Run("Create DeploymentTemplate (parameters)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - template := ` - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "289770176196104222", - "version": "0.30.23.60470" - } - }, - "parameters": { - "kubernetesNamespace": { - "defaultValue": "default", - "type": "string" - }, - "tag": { - "defaultValue": "latest", - "type": "string" - } - }, - "resources": { - "parameters": { - "import": "Radius", - "properties": { - "name": "parameters", - "properties": { - "compute": { - "kind": "kubernetes", - "namespace": "[parameters('kubernetesNamespace')]", - "resourceId": "self" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "[format('ghcr.io/radius-project/recipes/local-dev/rediscaches:{0}', parameters('tag'))]" - } - } - } - } - }, - "type": "Applications.Core/environments@2023-10-01-preview" - } - } - } - ` - var templateMap map[string]any - err := json.Unmarshal([]byte(template), &templateMap) + err = json.Unmarshal([]byte(template), &templateMap) require.NoError(t, err) - bicep := bicep.NewMockInterface(ctrl) - bicep.EXPECT(). - PrepareTemplate("parameters.bicep"). - Return(templateMap, nil). - Times(1) - - filePath := "parameters.bicep" - - outputSink := &output.MockOutput{} - runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - FileSystem: afero.NewMemMapFs(), - DeploymentScope: "/planes/radius/local/resourceGroups/default", - } - - fileExists, err := afero.Exists(runner.FileSystem, "parameters.yaml") + parameters, err := os.ReadFile(filepath.Join("testdata", testName, parametersFilePath)) require.NoError(t, err) - require.False(t, fileExists) - err = runner.Run(context.Background()) - require.NoError(t, err) - - fileExists, err = afero.Exists(runner.FileSystem, "parameters.yaml") - require.NoError(t, err) - require.True(t, fileExists) - - require.Equal(t, "parameters.yaml", runner.OutFile) - - expected, err := os.ReadFile(filepath.Join("testdata", "parameters", "parameters.yaml")) - require.NoError(t, err) - - actual, err := afero.ReadFile(runner.FileSystem, "parameters.yaml") - require.NoError(t, err) - require.Equal(t, string(expected), string(actual)) - }) - - t.Run("Create DeploymentTemplate (aws)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - template := ` - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "AWS": { - "provider": "AWS", - "version": "latest" - }, - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "4336724644513409792", - "version": "0.30.23.60470" - } - }, - "parameters": { - "bucketName": { - "defaultValue": "gkm-bucket", - "type": "string" - } - }, - "resources": { - "bucket": { - "import": "AWS", - "properties": { - "alias": "[parameters('bucketName')]", - "properties": { - "BucketName": "[parameters('bucketName')]" - } - }, - "type": "AWS.S3/Bucket@default" - } - } - } - ` - - var templateMap map[string]any - err := json.Unmarshal([]byte(template), &templateMap) - require.NoError(t, err) - - bicep := bicep.NewMockInterface(ctrl) - bicep.EXPECT(). - PrepareTemplate("aws.bicep"). - Return(templateMap, nil). - Times(1) - - filePath := "aws.bicep" - - outputSink := &output.MockOutput{} - runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - FileSystem: afero.NewMemMapFs(), - AWSScope: "awsscope", - DeploymentScope: "/planes/radius/local/resourceGroups/default", - } - - fileExists, err := afero.Exists(runner.FileSystem, "aws.yaml") - require.NoError(t, err) - require.False(t, fileExists) - - err = runner.Run(context.Background()) - require.NoError(t, err) - - fileExists, err = afero.Exists(runner.FileSystem, "aws.yaml") - require.NoError(t, err) - require.True(t, fileExists) - - require.Equal(t, "aws.yaml", runner.OutFile) - - expected, err := os.ReadFile(filepath.Join("testdata", "aws", "aws.yaml")) - require.NoError(t, err) - - actual, err := afero.ReadFile(runner.FileSystem, "aws.yaml") - require.NoError(t, err) - require.Equal(t, string(expected), string(actual)) - }) - - t.Run("Create DeploymentTemplate (azure)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - template := ` - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "14111843528652336728", - "version": "0.30.23.60470" - } - }, - "resources": { - "storageAccount": { - "apiVersion": "2021-04-01", - "kind": "StorageV2", - "location": "eastus", - "name": "gkmstorageaccount", - "properties": {}, - "sku": { - "name": "Standard_LRS" - }, - "type": "Microsoft.Storage/storageAccounts" - } - } - } - ` - - var templateMap map[string]any - err := json.Unmarshal([]byte(template), &templateMap) + var parametersMap map[string]map[string]any + err = json.Unmarshal([]byte(parameters), ¶metersMap) require.NoError(t, err) bicep := bicep.NewMockInterface(ctrl) bicep.EXPECT(). - PrepareTemplate("azure.bicep"). + PrepareTemplate(bicepFilePath). Return(templateMap, nil). Times(1) - filePath := "azure.bicep" - - outputSink := &output.MockOutput{} - runner := &Runner{ - Bicep: bicep, - Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - FileSystem: afero.NewMemMapFs(), - AzureScope: "azurescope", - DeploymentScope: "/planes/radius/local/resourceGroups/default", - } - - fileExists, err := afero.Exists(runner.FileSystem, "azure.yaml") - require.NoError(t, err) - require.False(t, fileExists) - - err = runner.Run(context.Background()) - require.NoError(t, err) - - fileExists, err = afero.Exists(runner.FileSystem, "azure.yaml") - require.NoError(t, err) - require.True(t, fileExists) - - require.Equal(t, "azure.yaml", runner.OutFile) - - expected, err := os.ReadFile(filepath.Join("testdata", "azure", "azure.yaml")) - require.NoError(t, err) - - actual, err := afero.ReadFile(runner.FileSystem, "azure.yaml") - require.NoError(t, err) - require.Equal(t, string(expected), string(actual)) - }) - - t.Run("Create DeploymentTemplate (module)", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - template := ` - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "1040374933922883026", - "version": "0.30.23.60470" - } - }, - "outputs": { - "storageAccountId": { - "type": "string", - "value": "[reference('storageModule').outputs.storageAccountId.value]" - } - }, - "parameters": { - "location": { - "defaultValue": "[resourceGroup().location]", - "type": "string" - }, - "storageAccountName": { - "type": "string" - } - }, - "resources": { - "storageModule": { - "apiVersion": "2022-09-01", - "name": "storageModule", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "17553429517046312167", - "version": "0.30.23.60470" - } - }, - "outputs": { - "storageAccountId": { - "type": "string", - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "storageAccountName": { - "type": "string" - } - }, - "resources": { - "storageAccount": { - "apiVersion": "2021-04-01", - "kind": "StorageV2", - "location": "[parameters('location')]", - "name": "[parameters('storageAccountName')]", - "properties": {}, - "sku": { - "name": "Standard_LRS" - }, - "type": "Microsoft.Storage/storageAccounts" - } - } - } - }, - "type": "Microsoft.Resources/deployments" - } - } - } - ` - - var templateMap map[string]any - err := json.Unmarshal([]byte(template), &templateMap) - require.NoError(t, err) - - bicep := bicep.NewMockInterface(ctrl) - bicep.EXPECT(). - PrepareTemplate("module.bicep"). - Return(templateMap, nil). - Times(1) - - filePath := "module.bicep" - outputSink := &output.MockOutput{} runner := &Runner{ Bicep: bicep, Output: outputSink, - FilePath: filePath, - Parameters: map[string]map[string]any{}, - FileSystem: afero.NewMemMapFs(), - DeploymentScope: "/planes/radius/local/resourceGroups/default", + FilePath: bicepFilePath, + Parameters: parametersMap, + FileSystem: filesystem.NewMemMapFileSystem(), + DestinationFile: yamlFilePath, + Group: resourceGroup, } - fileExists, err := afero.Exists(runner.FileSystem, "module.yaml") + fileExists := runner.FileSystem.Exists(yamlFilePath) require.NoError(t, err) require.False(t, fileExists) err = runner.Run(context.Background()) require.NoError(t, err) - fileExists, err = afero.Exists(runner.FileSystem, "module.yaml") + fileExists = runner.FileSystem.Exists(yamlFilePath) require.NoError(t, err) require.True(t, fileExists) - require.Equal(t, "module.yaml", runner.OutFile) + require.Equal(t, yamlFilePath, runner.DestinationFile) - expected, err := os.ReadFile(filepath.Join("testdata", "module", "module.yaml")) + expected, err := os.ReadFile(filepath.Join("testdata", testName, yamlFilePath)) require.NoError(t, err) - actual, err := afero.ReadFile(runner.FileSystem, "module.yaml") + actual, err := runner.FileSystem.ReadFile(yamlFilePath) require.NoError(t, err) require.Equal(t, string(expected), string(actual)) }) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep deleted file mode 100644 index 0261223e02..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.bicep +++ /dev/null @@ -1,11 +0,0 @@ -extension radius -extension aws - -param bucketName string = 'gkm-bucket' - -resource bucket 'AWS.S3/Bucket@default' = { - alias: bucketName - properties: { - BucketName: bucketName - } -} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml deleted file mode 100644 index 8c13b8bead..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/aws/aws.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: aws.bicep - namespace: radius-system -spec: - parameters: {} - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - }, - "aws": { - "type": "aws", - "value": { - "scope": "awsscope" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - } - } - rootFileName: aws.bicep - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "AWS": { - "provider": "AWS", - "version": "latest" - }, - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "4336724644513409792", - "version": "0.30.23.60470" - } - }, - "parameters": { - "bucketName": { - "defaultValue": "gkm-bucket", - "type": "string" - } - }, - "resources": { - "bucket": { - "import": "AWS", - "properties": { - "alias": "[parameters('bucketName')]", - "properties": { - "BucketName": "[parameters('bucketName')]" - } - }, - "type": "AWS.S3/Bucket@default" - } - } - } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep deleted file mode 100644 index df7267b823..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.bicep +++ /dev/null @@ -1,9 +0,0 @@ -resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { - name: 'gkmstorageaccount' - location: 'eastus' - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - properties: {} -} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml deleted file mode 100644 index 6eff218c64..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/azure/azure.yaml +++ /dev/null @@ -1,59 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: azure.bicep - namespace: radius-system -spec: - parameters: {} - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - }, - "az": { - "type": "azure", - "value": { - "scope": "azurescope" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - } - } - rootFileName: azure.bicep - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "14111843528652336728", - "version": "0.30.23.60470" - } - }, - "resources": { - "storageAccount": { - "apiVersion": "2021-04-01", - "kind": "StorageV2", - "location": "eastus", - "name": "gkmstorageaccount", - "properties": {}, - "sku": { - "name": "Standard_LRS" - }, - "type": "Microsoft.Storage/storageAccounts" - } - } - } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep deleted file mode 100644 index b753c3bb33..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.bicep +++ /dev/null @@ -1,20 +0,0 @@ -extension radius - -resource basic 'Applications.Core/environments@2023-10-01-preview' = { - name: 'basic' - properties: { - compute: { - kind: 'kubernetes' - resourceId: 'self' - namespace: 'default' - } - recipes: { - 'Applications.Datastores/redisCaches': { - default: { - templateKind: 'bicep' - templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest' - } - } - } - } -} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml deleted file mode 100644 index b09c2f74f9..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/basic/basic.yaml +++ /dev/null @@ -1,70 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: basic.bicep - namespace: radius-system -spec: - parameters: {} - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - } - } - rootFileName: basic.bicep - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "10886769892319697000", - "version": "0.30.23.60470" - } - }, - "resources": { - "basic": { - "import": "Radius", - "properties": { - "name": "basic", - "properties": { - "compute": { - "kind": "kubernetes", - "namespace": "default", - "resourceId": "self" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "ghcr.io/radius-project/recipes/local-dev/rediscaches:latest" - } - } - } - } - }, - "type": "Applications.Core/environments@2023-10-01-preview" - } - } - } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate-parameters.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate-parameters.json new file mode 100644 index 0000000000..4f773154ac --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate-parameters.json @@ -0,0 +1,5 @@ +{ + "tag": { + "value": "v1.0.0" + } +} diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.bicep similarity index 84% rename from pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep rename to pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.bicep index 6702df6ea9..8af9edc02f 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.bicep +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.bicep @@ -15,7 +15,7 @@ resource parameters 'Applications.Core/environments@2023-10-01-preview' = { 'Applications.Datastores/redisCaches': { default: { templateKind: 'bicep' - templatePath: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:${tag}' + templatePath: 'ghcr.io/myregistry:${tag}' } } } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json new file mode 100644 index 0000000000..0fbfcc324e --- /dev/null +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.32.4.45862", + "templateHash": "16344337442844554850" + } + }, + "parameters": { + "tag": { + "type": "string", + "defaultValue": "latest" + }, + "kubernetesNamespace": { + "type": "string", + "defaultValue": "default" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "parameters": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "parameters", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('kubernetesNamespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('ghcr.io/myregistry:{0}', parameters('tag'))]" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml similarity index 88% rename from pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml rename to pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml index 307ba72c96..20bb06dd19 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.yaml +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml @@ -1,10 +1,11 @@ apiVersion: radapp.io/v1alpha3 kind: DeploymentTemplate metadata: - name: parameters.bicep + name: deploymenttemplate.bicep namespace: radius-system spec: - parameters: {} + parameters: + tag: v1.0.0 providerConfig: |- { "radius": { @@ -20,7 +21,7 @@ spec: } } } - rootFileName: parameters.bicep + rootFileName: deploymenttemplate.bicep template: |- { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", @@ -39,8 +40,8 @@ spec: "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", "_generator": { "name": "bicep", - "templateHash": "289770176196104222", - "version": "0.30.23.60470" + "templateHash": "16344337442844554850", + "version": "0.32.4.45862" } }, "parameters": { @@ -68,7 +69,7 @@ spec: "Applications.Datastores/redisCaches": { "default": { "templateKind": "bicep", - "templatePath": "[format('ghcr.io/radius-project/recipes/local-dev/rediscaches:{0}', parameters('tag'))]" + "templatePath": "[format('ghcr.io/myregistry:{0}', parameters('tag'))]" } } } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep deleted file mode 100644 index 3758e69214..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.bicep +++ /dev/null @@ -1,14 +0,0 @@ -param location string = resourceGroup().location -param storageAccountName string - -// Import the storage module -module storageModule 'storage.bicep' = { - name: 'storageModule' - params: { - location: location - storageAccountName: storageAccountName - } -} - -// Output the storage account ID -output storageAccountId string = storageModule.outputs.storageAccountId diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml deleted file mode 100644 index 4cd0ad4284..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/module.yaml +++ /dev/null @@ -1,119 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: module.bicep - namespace: radius-system -spec: - parameters: {} - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/default" - } - } - } - rootFileName: module.bicep - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "1040374933922883026", - "version": "0.30.23.60470" - } - }, - "outputs": { - "storageAccountId": { - "type": "string", - "value": "[reference('storageModule').outputs.storageAccountId.value]" - } - }, - "parameters": { - "location": { - "defaultValue": "[resourceGroup().location]", - "type": "string" - }, - "storageAccountName": { - "type": "string" - } - }, - "resources": { - "storageModule": { - "apiVersion": "2022-09-01", - "name": "storageModule", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "storageAccountName": { - "value": "[parameters('storageAccountName')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "17553429517046312167", - "version": "0.30.23.60470" - } - }, - "outputs": { - "storageAccountId": { - "type": "string", - "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "storageAccountName": { - "type": "string" - } - }, - "resources": { - "storageAccount": { - "apiVersion": "2021-04-01", - "kind": "StorageV2", - "location": "[parameters('location')]", - "name": "[parameters('storageAccountName')]", - "properties": {}, - "sku": { - "name": "Standard_LRS" - }, - "type": "Microsoft.Storage/storageAccounts" - } - } - } - }, - "type": "Microsoft.Resources/deployments" - } - } - } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep deleted file mode 100644 index 9a608dfdfc..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/module/storage.bicep +++ /dev/null @@ -1,14 +0,0 @@ -param location string -param storageAccountName string - -resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = { - name: storageAccountName - location: location - sku: { - name: 'Standard_LRS' - } - kind: 'StorageV2' - properties: {} -} - -output storageAccountId string = storageAccount.id diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json index 15bf94db44..cb278882b6 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters.json @@ -3,7 +3,7 @@ "contentVersion": "1.0.0.0", "parameters": { "b": { - "value": "b" + "value": "c" } } } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json deleted file mode 100644 index b822e3383d..0000000000 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/parameters/parameters.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "tag": { - "value": "notlatest" - } - } -} diff --git a/pkg/cli/cmd/deploy/deploy.go b/pkg/cli/cmd/deploy/deploy.go index b2216d1f9d..e4377ca992 100644 --- a/pkg/cli/cmd/deploy/deploy.go +++ b/pkg/cli/cmd/deploy/deploy.go @@ -30,12 +30,12 @@ import ( "github.com/radius-project/radius/pkg/cli/cmd/commonflags" "github.com/radius-project/radius/pkg/cli/connections" "github.com/radius-project/radius/pkg/cli/deploy" + "github.com/radius-project/radius/pkg/cli/filesystem" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/to" - "github.com/spf13/afero" "github.com/spf13/cobra" "golang.org/x/exp/maps" ) @@ -236,7 +236,7 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } - parser := bicep.ParameterParser{FileSystem: afero.NewOsFs()} + parser := bicep.ParameterParser{FileSystem: filesystem.NewOSFS()} r.Parameters, err = parser.Parse(parameterArgs...) if err != nil { return err diff --git a/pkg/cli/cmd/recipe/register/register.go b/pkg/cli/cmd/recipe/register/register.go index ce3c39633d..7d4682a9ca 100644 --- a/pkg/cli/cmd/recipe/register/register.go +++ b/pkg/cli/cmd/recipe/register/register.go @@ -19,13 +19,12 @@ package register import ( "context" - "github.com/spf13/afero" - "github.com/radius-project/radius/pkg/cli" "github.com/radius-project/radius/pkg/cli/bicep" "github.com/radius-project/radius/pkg/cli/clierrors" "github.com/radius-project/radius/pkg/cli/cmd/commonflags" "github.com/radius-project/radius/pkg/cli/connections" + "github.com/radius-project/radius/pkg/cli/filesystem" "github.com/radius-project/radius/pkg/cli/framework" "github.com/radius-project/radius/pkg/cli/output" "github.com/radius-project/radius/pkg/cli/workspaces" @@ -150,7 +149,7 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { return err } - parser := bicep.ParameterParser{FileSystem: afero.NewOsFs()} + parser := bicep.ParameterParser{FileSystem: filesystem.NewOSFS()} r.Parameters, err = parser.Parse(parameterArgs...) if err != nil { return err diff --git a/pkg/cli/filesystem/filesystem.go b/pkg/cli/filesystem/filesystem.go new file mode 100644 index 0000000000..aedb40b1e2 --- /dev/null +++ b/pkg/cli/filesystem/filesystem.go @@ -0,0 +1,15 @@ +package filesystem + +import ( + "io/fs" +) + +// FileSystem is an interface that defines the methods needed to interact with a file system. +type FileSystem interface { + Create(name string) (fs.File, error) + Exists(name string) bool + Open(name string) (fs.File, error) + ReadFile(name string) ([]byte, error) + Stat(name string) (fs.FileInfo, error) + WriteFile(name string, data []byte, perm fs.FileMode) error +} diff --git a/pkg/cli/filesystem/memmapfs.go b/pkg/cli/filesystem/memmapfs.go new file mode 100644 index 0000000000..9bf9e329bc --- /dev/null +++ b/pkg/cli/filesystem/memmapfs.go @@ -0,0 +1,52 @@ +package filesystem + +import ( + "io/fs" + "testing/fstest" +) + +// MemMapFileSystem is an implementation of the FileSystem interface that uses an in-memory map to store files. +// It uses the methods from the fstest package to interact with the in-memory map. +type MemMapFileSystem struct { + InternalFileSystem fstest.MapFS +} + +var _ FileSystem = (*MemMapFileSystem)(nil) + +func NewMemMapFileSystem() *MemMapFileSystem { + return &MemMapFileSystem{ + InternalFileSystem: fstest.MapFS{}, + } +} + +func (mmfs MemMapFileSystem) Create(name string) (fs.File, error) { + mmfs.InternalFileSystem[name] = &fstest.MapFile{} + + return mmfs.InternalFileSystem.Open(name) +} + +func (mmfs MemMapFileSystem) Exists(name string) bool { + _, ok := mmfs.InternalFileSystem[name] + return ok +} + +func (mmfs MemMapFileSystem) Open(name string) (fs.File, error) { + return mmfs.InternalFileSystem.Open(name) +} + +func (mmfs MemMapFileSystem) ReadFile(name string) ([]byte, error) { + return mmfs.InternalFileSystem.ReadFile(name) +} + +func (mmfs MemMapFileSystem) Stat(name string) (fs.FileInfo, error) { + return mmfs.InternalFileSystem.Stat(name) +} + +func (mmfs MemMapFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error { + mmfs.InternalFileSystem[name] = &fstest.MapFile{ + Data: data, + Mode: perm, + } + + return nil +} diff --git a/pkg/cli/filesystem/memmapfs_test.go b/pkg/cli/filesystem/memmapfs_test.go new file mode 100644 index 0000000000..c48539208b --- /dev/null +++ b/pkg/cli/filesystem/memmapfs_test.go @@ -0,0 +1,83 @@ +package filesystem + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewMemMapFileSystem(t *testing.T) { + fs := NewMemMapFileSystem() + require.NotNil(t, fs) +} + +func TestMemMapFileSystem_Create(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + + file, err := fs.Create(fileName) + require.NoError(t, err) + require.NotNil(t, file) + require.True(t, fs.Exists(fileName)) +} + +func TestMemMapFileSystem_Exists(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + + require.False(t, fs.Exists(fileName)) + + _, _ = fs.Create(fileName) + + require.True(t, fs.Exists(fileName)) +} + +func TestMemMapFileSystem_Open(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + + _, _ = fs.Create(fileName) + + file, err := fs.Open(fileName) + require.NoError(t, err) + require.NotNil(t, file) +} + +func TestMemMapFileSystem_ReadFile(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + data := []byte("hello world") + + err := fs.WriteFile(fileName, data, os.ModePerm) + require.NoError(t, err) + + readData, err := fs.ReadFile(fileName) + require.NoError(t, err) + require.Equal(t, data, readData) +} + +func TestMemMapFileSystem_Stat(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + + _, _ = fs.Create(fileName) + + info, err := fs.Stat(fileName) + require.NoError(t, err) + require.NotNil(t, info) + require.Equal(t, fileName, info.Name()) +} + +func TestMemMapFileSystem_WriteFile(t *testing.T) { + fs := NewMemMapFileSystem() + fileName := "testfile" + data := []byte("hello world") + + err := fs.WriteFile(fileName, data, os.ModePerm) + require.NoError(t, err) + + readData, err := fs.ReadFile(fileName) + require.NoError(t, err) + require.Equal(t, data, readData) +} diff --git a/pkg/cli/filesystem/osfs.go b/pkg/cli/filesystem/osfs.go new file mode 100644 index 0000000000..3647e17844 --- /dev/null +++ b/pkg/cli/filesystem/osfs.go @@ -0,0 +1,41 @@ +package filesystem + +import ( + "io/fs" + "os" +) + +// OSFileSystem is an implementation of the FileSystem interface that uses the OS filesystem. +// It uses the methods from the os package to interact with the filesystem. +type OSFileSystem struct{} + +var _ FileSystem = (*OSFileSystem)(nil) + +func NewOSFS() *OSFileSystem { + return &OSFileSystem{} +} + +func (osfs OSFileSystem) Create(name string) (fs.File, error) { + return os.Create(name) +} + +func (osfs OSFileSystem) Exists(name string) bool { + _, err := os.Stat(name) + return err != nil +} + +func (osfs OSFileSystem) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func (osfs OSFileSystem) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} + +func (osfs OSFileSystem) Stat(name string) (fs.FileInfo, error) { + return os.Stat(name) +} + +func (osfs OSFileSystem) WriteFile(name string, data []byte, perm fs.FileMode) error { + return os.WriteFile(name, data, perm) +} diff --git a/pkg/controller/reconciler/const.go b/pkg/controller/reconciler/const.go index 2d1da9f6f3..77a39df103 100644 --- a/pkg/controller/reconciler/const.go +++ b/pkg/controller/reconciler/const.go @@ -53,7 +53,4 @@ const ( // DeploymentResourceFinalizer is the name of the finalizer added to DeploymentResources. DeploymentResourceFinalizer = "radapp.io/deployment-resource-finalizer" - - // RadiusSystemNamespace is the name of the system namespace where Radius resources are stored. - RadiusSystemNamespace = "radius-system" ) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 793d0e1fb7..2aefc39bbe 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -24,6 +24,7 @@ import ( "time" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -35,6 +36,7 @@ import ( "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/ucplog" corev1 "k8s.io/api/core/v1" ) @@ -235,49 +237,21 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl // fully processed any status changes until the async operation completes. deploymentResource.Status.ObservedGeneration = deploymentResource.Generation - // NOTE: The following is a workaround for Radius API behavior. Since deleting - // an application or environment can leave hanging resources, we need to make sure to - // delete these resources before deleting the application or environment. - - // Check other resources that depend on this resource. - // List all DeploymentResource objects in the same namespace - // that have the same rootFileName. - deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} - err := r.Client.List(ctx, deploymentResourceList, client.InNamespace(deploymentResource.Namespace), client.MatchingFields{rootFileNameField: deploymentResource.Spec.RootFileName}) + // Check if the resource is being used by another resource + deploymentResourceList, err := listResourcesWithSameOwner(ctx, r.Client, deploymentResource.Namespace, deploymentResource.OwnerReferences[0]) if err != nil { - return ctrl.Result{}, nil - } - - appsCount := 0 - envsCount := 0 - otherCount := 0 - for _, dr := range deploymentResourceList.Items { - if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { - continue - } - if strings.Contains(dr.Spec.Id, "Applications.Core/applications") { - appsCount++ - } else if strings.Contains(dr.Spec.Id, "Applications.Core/environments") { - envsCount++ - } else if dr.Spec.Id != "" { - logger.Info("Resource is being used by another resource.", "resourceId", dr.Spec.Id) - otherCount++ - } + return ctrl.Result{}, err } - if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/applications") { - // dont delete app until otherCount is 0 - if otherCount > 0 { - logger.Info("Resource is an application, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } + // Check if the resource is being used by another resource + dependentResource, err := checkForDeploymentResourceDependencies(deploymentResource, deploymentResourceList) + if err != nil { + return ctrl.Result{}, err } - if strings.Contains(deploymentResource.Spec.Id, "Applications.Core/environments") { - if otherCount > 0 { - logger.Info("Resource is an environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id) - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil - } + if dependentResource != "" { + logger.Info("Resource is an application or environment, being used by another resource.", "resourceId", deploymentResource.Spec.Id, "dependentResource", dependentResource) + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } poller, err := r.startDeleteOperation(ctx, deploymentResource) @@ -354,21 +328,75 @@ func (r *DeploymentResourceReconciler) requeueDelay() time.Duration { return delay } -func deploymentResourceRootFileNameIndexer(o client.Object) []string { - deploymentResource, ok := o.(*radappiov1alpha3.DeploymentResource) - if !ok { - return nil - } - return []string{deploymentResource.Spec.RootFileName} -} - // SetupWithManager sets up the controller with the Manager. func (r *DeploymentResourceReconciler) SetupWithManager(mgr ctrl.Manager) error { - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &radappiov1alpha3.DeploymentResource{}, rootFileNameField, deploymentResourceRootFileNameIndexer); err != nil { - return err - } - return ctrl.NewControllerManagedBy(mgr). For(&radappiov1alpha3.DeploymentResource{}). Complete(r) } + +func listResourcesWithSameOwner(ctx context.Context, c client.Client, namespace string, ownerRef metav1.OwnerReference) ([]radappiov1alpha3.DeploymentResource, error) { + // List all DeploymentResource objects in the same namespace + deploymentResourceList := &radappiov1alpha3.DeploymentResourceList{} + err := c.List(ctx, deploymentResourceList, client.InNamespace(namespace)) + if err != nil { + return nil, err + } + + // Filter resources based on OwnerReference + var filteredResources []radappiov1alpha3.DeploymentResource + for _, dr := range deploymentResourceList.Items { + for _, or := range dr.OwnerReferences { + if or.UID == ownerRef.UID { + filteredResources = append(filteredResources, dr) + break + } + } + } + + return filteredResources, nil +} + +// checkForDeploymentResourceDependencies checks if the deploymentResource is an application or environment. +// If it is, it checks if other (non-application or environment) resources exist. +// If other resources exist, it returns the ID of one of the dependent resources. +// NOTE: This is a workaround for Radius API behavior. Since deleting +// an application or environment can leave hanging resources, we need to make sure to +// delete these resources before deleting the application or environment. +// https://github.com/radius-project/radius/issues/8164 +func checkForDeploymentResourceDependencies(deploymentResource *radappiov1alpha3.DeploymentResource, deploymentResourceList []radappiov1alpha3.DeploymentResource) (string, error) { + deploymentResourceID, err := resources.ParseResource(deploymentResource.Spec.Id) + if err != nil { + return "", err + } + + if strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/applications") { + return "", nil + } + + if strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/environments") { + return "", nil + } + + resourceCount := 0 + dependentResource := "" + for _, dr := range deploymentResourceList { + // shouldn't need this... + // if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { + // continue + // } + + id, err := resources.ParseResource(dr.Spec.Id) + if err != nil { + return "", err + } + + // don't count applications or environments + if !strings.EqualFold(id.Type(), "Applications.Core/applications") && !strings.EqualFold(id.Type(), "Applications.Core/environments") { + resourceCount++ + dependentResource = dr.Spec.Id + } + } + + return dependentResource, nil +} diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index c7838ac654..ece57923f4 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -192,7 +192,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d if _, ok := existingOutputResources[outputResourceId]; !ok { // Resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it - resourceName := generateDeploymentResourceName(outputResourceId) + resourceName, err := generateDeploymentResourceName(outputResourceId) + if err != nil { + return ctrl.Result{}, err + } + deploymentResource := &radappiov1alpha3.DeploymentResource{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, @@ -224,8 +228,12 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d if _, ok := newOutputResources[resource]; !ok { // Resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it logger.Info("Deleting resource.", "resourceId", resource) - resourceName := generateDeploymentResourceName(resource) - err := r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ + resourceName, err := generateDeploymentResourceName(resource) + if err != nil { + return ctrl.Result{}, err + } + + err = r.Client.Delete(ctx, &radappiov1alpha3.DeploymentResource{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: deploymentTemplate.Namespace, diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json index 910cb2fa0c..222ede5618 100644 --- a/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.31.92.45157", @@ -36,4 +34,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkg/controller/reconciler/util.go b/pkg/controller/reconciler/util.go index 93bd8e0a92..91c027d1e1 100644 --- a/pkg/controller/reconciler/util.go +++ b/pkg/controller/reconciler/util.go @@ -281,10 +281,13 @@ func createOrUpdateContainer(ctx context.Context, radius RadiusClient, container return nil, nil } -func generateDeploymentResourceName(resourceId string) string { - resourceBaseName := strings.Split(resourceId, "/")[len(strings.Split(resourceId, "/"))-1] +func generateDeploymentResourceName(resourceId string) (string, error) { + id, err := resources.ParseResource(resourceId) + if err != nil { + return "", err + } - return resourceBaseName + return id.Name(), nil } func convertToARMJSONParameters(parameters map[string]string) map[string]map[string]string { diff --git a/pkg/controller/reconciler/util_test.go b/pkg/controller/reconciler/util_test.go new file mode 100644 index 0000000000..546d27c416 --- /dev/null +++ b/pkg/controller/reconciler/util_test.go @@ -0,0 +1,88 @@ +package reconciler + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerateDeploymentResourceName(t *testing.T) { + tests := []struct { + name string + resourceId string + want string + wantErr bool + }{ + { + name: "valid resource ID", + resourceId: "/subscriptions/123/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/mySite", + want: "mySite", + wantErr: false, + }, + { + name: "invalid resource ID", + resourceId: "invalidResourceId", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := generateDeploymentResourceName(tt.resourceId) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestConvertToARMJSONParameters(t *testing.T) { + tests := []struct { + name string + parameters map[string]string + want map[string]map[string]string + }{ + { + name: "single parameter", + parameters: map[string]string{ + "param1": "value1", + }, + want: map[string]map[string]string{ + "param1": { + "value": "value1", + }, + }, + }, + { + name: "multiple parameters", + parameters: map[string]string{ + "param1": "value1", + "param2": "value2", + }, + want: map[string]map[string]string{ + "param1": { + "value": "value1", + }, + "param2": { + "value": "value2", + }, + }, + }, + { + name: "empty parameters", + parameters: map[string]string{}, + want: map[string]map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertToARMJSONParameters(tt.parameters) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index ddaff9de46..8710f08535 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -19,7 +19,10 @@ package kubernetes_test import ( "context" "encoding/json" + "fmt" + "os" "path" + "strings" "testing" "time" @@ -31,7 +34,8 @@ import ( sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/test/rp" "github.com/radius-project/radius/test/testcontext" - "github.com/spf13/afero" + "github.com/radius-project/radius/test/testutil" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -45,102 +49,210 @@ import ( controller_runtime "sigs.k8s.io/controller-runtime/pkg/client" ) -func Test_DeploymentTemplate(t *testing.T) { - defaultProviderConfig, err := generateDefaultProviderConfig() +func Test_DeploymentTemplate_Basic(t *testing.T) { + ctx := testcontext.New(t) + opts := rp.NewRPTestOptions(t) + + name := "dt-env" + namespace := "dt-env-ns" + fileName := "env.bicep" + templateFilePath := path.Join("testdata", "env", "env.json") + parameters := []string{ + fmt.Sprintf("name=%s", name), + fmt.Sprintf("namespace=%s", namespace), + } + + providerConfig, err := generateDefaultProviderConfig() require.NoError(t, err) - testcases := []struct { - name string - namespace string - fileName string - templateFilePath string - providerConfig string - parameters map[string]string - }{ - { - name: "dt-env", - namespace: "dt-ns-env", - fileName: "env.bicep", - templateFilePath: path.Join("testdata", "env", "env.json"), - providerConfig: defaultProviderConfig, - parameters: map[string]string{ - "name": "dt-env", - "namespace": "dt-ns-env", - }, - }, - { - name: "dt-module", - namespace: "dt-ns-module", - fileName: "module.bicep", - templateFilePath: path.Join("testdata", "module", "module.json"), - providerConfig: defaultProviderConfig, - parameters: map[string]string{ - "name": "dt-module", - "namespace": "dt-ns-module", - }, - }, + parametersMap := createParametersMap(parameters) + + template, err := os.ReadFile(templateFilePath) + require.NoError(t, err) + + // Create the namespace, if it already exists we can ignore the error. + _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) + + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) + + t.Run("Create DeploymentTemplate", func(t *testing.T) { + t.Log("Creating DeploymentTemplate") + err = opts.Client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + }) + + t.Run("Check DeploymentTemplate status", func(t *testing.T) { + ctx, cancel := testcontext.NewWithCancel(t) + defer cancel() + + // Get resource version + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + require.NoError(t, err) + + t.Log("Waiting for DeploymentTemplate ready") + deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) + require.NoError(t, err) + + scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + require.NoError(t, err) + + expectedResources := [][]string{ + {"Applications.Core/environments", fmt.Sprintf("%s-env", name)}, + } + + assertExpectedResourcesExist(t, ctx, scope, expectedResources, opts.Connection) + }) + + t.Run("Delete DeploymentTemplate", func(t *testing.T) { + t.Log("Deleting DeploymentTemplate") + err = opts.Client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) + + require.Eventually(t, func() bool { + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + return apierrors.IsNotFound(err) + }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }) +} + +func Test_DeploymentTemplate_Module(t *testing.T) { + ctx := testcontext.New(t) + opts := rp.NewRPTestOptions(t) + + name := "dt-module" + namespace := "dt-module-ns" + fileName := "module.bicep" + templateFilePath := path.Join("testdata", "module", "module.json") + parameters := []string{ + "name=dt-module", + "namespace=dt-ns-module", } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - ctx := testcontext.New(t) - opts := rp.NewRPTestOptions(t) + providerConfig, err := generateDefaultProviderConfig() + require.NoError(t, err) - name := tc.name - namespace := tc.namespace + parametersMap := createParametersMap(parameters) - template, err := afero.ReadFile(afero.NewOsFs(), tc.templateFilePath) - require.NoError(t, err) + template, err := os.ReadFile(templateFilePath) + require.NoError(t, err) - // Create the namespace, if it already exists we can ignore the error. - _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) - require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) + // Create the namespace, if it already exists we can ignore the error. + _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) - deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), tc.providerConfig, tc.fileName, tc.parameters) + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) - t.Run("Deploy", func(t *testing.T) { - t.Log("Creating DeploymentTemplate") - err = opts.Client.Create(ctx, deploymentTemplate) - require.NoError(t, err) - }) + t.Run("Create DeploymentTemplate", func(t *testing.T) { + t.Log("Creating DeploymentTemplate") + err = opts.Client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + }) - t.Run("Check status", func(t *testing.T) { - ctx, cancel := testcontext.NewWithCancel(t) - defer cancel() + t.Run("Check DeploymentTemplate status", func(t *testing.T) { + ctx, cancel := testcontext.NewWithCancel(t) + defer cancel() - // Get resource version - err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) - require.NoError(t, err) + // Get resource version + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + require.NoError(t, err) - t.Log("Waiting for DeploymentTemplate ready") - deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) - require.NoError(t, err) + t.Log("Waiting for DeploymentTemplate ready") + deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) + require.NoError(t, err) - // Doing a basic check that the deploymentTemplate has a resource provisioned. - require.NotEmpty(t, deploymentTemplate.Status.Resource) + scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + require.NoError(t, err) - scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) - require.NoError(t, err) + expectedResources := [][]string{ + {"Applications.Core/environments", fmt.Sprintf("%s-env", name)}, + {"Applications.Core/applications", fmt.Sprintf("%s-app", name)}, + } - client, err := generated.NewGenericResourcesClient(scope, "Applications.Core/environments", &aztoken.AnonymousCredential{}, sdk.NewClientOptions(opts.Connection)) - require.NoError(t, err) + assertExpectedResourcesExist(t, ctx, scope, expectedResources, opts.Connection) + }) - _, err = client.Get(ctx, deploymentTemplate.Name, nil) - require.NoError(t, err) - }) + t.Run("Delete DeploymentTemplate", func(t *testing.T) { + t.Log("Deleting DeploymentTemplate") + err = opts.Client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) - t.Run("Delete", func(t *testing.T) { - t.Log("Deleting DeploymentTemplate") - err = opts.Client.Delete(ctx, deploymentTemplate) - require.NoError(t, err) + require.Eventually(t, func() bool { + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + return apierrors.IsNotFound(err) + }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }) +} - require.Eventually(t, func() bool { - err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) - return apierrors.IsNotFound(err) - }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") - }) - }) +func Test_DeploymentTemplate_Recipe(t *testing.T) { + ctx := testcontext.New(t) + opts := rp.NewRPTestOptions(t) + + name := "dt-recipe" + namespace := "dt-recipe-ns" + fileName := "recipe.bicep" + templateFilePath := path.Join("testdata", "recipe", "recipe.json") + parameters := []string{ + testutil.GetBicepRecipeRegistry(), + testutil.GetBicepRecipeVersion(), + "name=dt-recipe", + "namespace=dt-ns-recipe", } + + providerConfig, err := generateDefaultProviderConfig() + require.NoError(t, err) + + parametersMap := createParametersMap(parameters) + + template, err := os.ReadFile(templateFilePath) + require.NoError(t, err) + + // Create the namespace, if it already exists we can ignore the error. + _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) + require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) + + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) + + t.Run("Create DeploymentTemplate", func(t *testing.T) { + t.Log("Creating DeploymentTemplate") + err = opts.Client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + }) + + t.Run("Check DeploymentTemplate status", func(t *testing.T) { + ctx, cancel := testcontext.NewWithCancel(t) + defer cancel() + + // Get resource version + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + require.NoError(t, err) + + t.Log("Waiting for DeploymentTemplate ready") + deploymentTemplate, err := waitForDeploymentTemplateReady(t, ctx, types.NamespacedName{Name: name, Namespace: namespace}, opts.Client, deploymentTemplate.ResourceVersion) + require.NoError(t, err) + + scope, err := reconciler.ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) + require.NoError(t, err) + + expectedResources := [][]string{ + {"Applications.Core/environments", fmt.Sprintf("%s-env", name)}, + {"Applications.Core/applications", fmt.Sprintf("%s-app", name)}, + {"Applications.Datastores/redisCaches", fmt.Sprintf("%s-recipe", name)}, + } + + assertExpectedResourcesExist(t, ctx, scope, expectedResources, opts.Connection) + }) + + t.Run("Delete DeploymentTemplate", func(t *testing.T) { + t.Log("Deleting DeploymentTemplate") + err = opts.Client.Delete(ctx, deploymentTemplate) + require.NoError(t, err) + + require.Eventually(t, func() bool { + err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) + return apierrors.IsNotFound(err) + }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }) } func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig, rootFileName string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { @@ -221,3 +333,30 @@ func generateDefaultProviderConfig() (string, error) { } return string(marshalledProviderConfig), nil } + +func createParametersMap(parameters []string) map[string]string { + parametersMap := make(map[string]string) + for _, param := range parameters { + kv := strings.Split(param, "=") + key := kv[0] + value := kv[1] + parametersMap[key] = value + } + + return parametersMap +} + +// assertExpectedResourcesExist asserts that the expected resources exist +// in Radius for the given scope. +func assertExpectedResourcesExist(t *testing.T, ctx context.Context, scope string, expectedResources [][]string, connection sdk.Connection) { + for _, resource := range expectedResources { + resourceType := resource[0] + resourceName := resource[1] + + client, err := generated.NewGenericResourcesClient(scope, resourceType, &aztoken.AnonymousCredential{}, sdk.NewClientOptions(connection)) + require.NoError(t, err) + + _, err = client.Get(ctx, resourceName, nil) + require.NoError(t, err) + } +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep b/test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep index 8f2aa8df73..2044caa2bc 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/env/env.bicep @@ -4,7 +4,7 @@ param name string param namespace string resource env 'Applications.Core/environments@2023-10-01-preview' = { - name: name + name: '${name}-env' properties: { compute: { kind: 'kubernetes' diff --git a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json index 5fc46b7113..8fb3e9cbea 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json @@ -4,11 +4,13 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", - "version": "0.31.92.45157", - "templateHash": "10640932074887270592" + "version": "0.32.4.45862", + "templateHash": "17296380169561690776" } }, "parameters": { @@ -30,7 +32,7 @@ "import": "Radius", "type": "Applications.Core/environments@2023-10-01-preview", "properties": { - "name": "[parameters('name')]", + "name": "[format('{0}-env', parameters('name'))]", "properties": { "compute": { "kind": "kubernetes", @@ -41,4 +43,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep index 08a0860458..6fbd41584b 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep @@ -1,17 +1,13 @@ extension radius param name string -param namespace string +param envId string -resource env 'Applications.Core/environments@2023-10-01-preview' = { - name: name +resource app 'Applications.Core/applications@2023-10-01-preview' = { + name: '${name}-app' properties: { - compute: { - kind: 'kubernetes' - resourceId: 'self' - namespace: namespace - } + environment: envId } } -output envId string = env.id +output appId string = app.id diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep index 2f8bb39528..21e48b3a1a 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep @@ -1,13 +1,23 @@ +extension radius + param name string param namespace string +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: '${name}-env' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: namespace + } + } +} + module module 'module-dependency.bicep' = { name: 'module' params: { name: name - namespace: namespace + envId: env.id } } - -// Output the storage account ID -output envId string = module.outputs.envId diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index 9e2a2654b6..94c52f10fc 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -4,11 +4,13 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", - "version": "0.31.92.45157", - "templateHash": "17470534345050893154" + "version": "0.32.4.45862", + "templateHash": "18123004600558185514" } }, "parameters": { @@ -19,7 +21,27 @@ "type": "string" } }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "[format('{0}-env', parameters('name'))]", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('namespace')]" + } + } + } + }, "module": { "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", @@ -33,8 +55,8 @@ "name": { "value": "[parameters('name')]" }, - "namespace": { - "value": "[parameters('namespace')]" + "envId": { + "value": "[reference('env').id]" } }, "template": { @@ -43,18 +65,20 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", - "version": "0.31.92.45157", - "templateHash": "13226276940735477072" + "version": "0.32.4.45862", + "templateHash": "6618993452395382629" } }, "parameters": { "name": { "type": "string" }, - "namespace": { + "envId": { "type": "string" } }, @@ -65,35 +89,28 @@ } }, "resources": { - "env": { + "app": { "import": "Radius", - "type": "Applications.Core/environments@2023-10-01-preview", + "type": "Applications.Core/applications@2023-10-01-preview", "properties": { - "name": "[parameters('name')]", + "name": "[format('{0}-app', parameters('name'))]", "properties": { - "compute": { - "kind": "kubernetes", - "resourceId": "self", - "namespace": "[parameters('namespace')]" - } + "environment": "[parameters('envId')]" } } } }, "outputs": { - "envId": { + "appId": { "type": "string", - "value": "[reference('env').id]" + "value": "[reference('app').id]" } } } - } - } - }, - "outputs": { - "envId": { - "type": "string", - "value": "[reference('module').outputs.envId.value]" + }, + "dependsOn": [ + "env" + ] } } -} +} \ No newline at end of file diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep new file mode 100644 index 0000000000..2dfca85c9a --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep @@ -0,0 +1,40 @@ +extension radius + +param name string +param namespace string +param registry string +param version string + +resource env 'Applications.Core/environments@2023-10-01-preview' = { + name: '${name}-env' + properties: { + compute: { + kind: 'kubernetes' + resourceId: 'self' + namespace: namespace + } + recipes: { + 'Applications.Datastores/redisCaches': { + default: { + templateKind: 'bicep' + templatePath: '${registry}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:${version}' + } + } + } + } +} + +resource app 'Applications.Core/applications@2023-10-01-preview' = { + name: '${name}-app' + properties: { + environment: env.id + } +} + +resource recipe 'Applications.Datastores/redisCaches@2023-10-01-preview' = { + name: '${name}-recipe' + properties: { + application: app.id + environment: env.id + } +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json new file mode 100644 index 0000000000..df572c61cb --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.32.4.45862", + "templateHash": "11546695473811123326" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "[format('{0}-env', parameters('name'))]", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "[parameters('namespace')]" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('{0}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:{1}', parameters('registry'), parameters('version'))]" + } + } + } + } + } + }, + "app": { + "import": "Radius", + "type": "Applications.Core/applications@2023-10-01-preview", + "properties": { + "name": "[format('{0}-app', parameters('name'))]", + "properties": { + "environment": "[reference('env').id]" + } + }, + "dependsOn": [ + "env" + ] + }, + "recipe": { + "import": "Radius", + "type": "Applications.Datastores/redisCaches@2023-10-01-preview", + "properties": { + "name": "[format('{0}-recipe', parameters('name'))]", + "properties": { + "application": "[reference('app').id]", + "environment": "[reference('env').id]" + } + }, + "dependsOn": [ + "app", + "env" + ] + } + } +} \ No newline at end of file From ab44f4f61ee9e6a38ad34c6472272fa4bba8e500 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 23 Dec 2024 14:47:32 -0800 Subject: [PATCH 34/65] PR Signed-off-by: willdavsmith --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c6488f45bb..6c9b180436 100644 --- a/go.mod +++ b/go.mod @@ -145,6 +145,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -291,7 +292,6 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/afero v1.11.0 github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect @@ -329,7 +329,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apiserver v0.31.3 // indirect k8s.io/component-base v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect From be0a636434bf468363431773f059e2aa855b9513 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 23 Dec 2024 14:50:13 -0800 Subject: [PATCH 35/65] PR Signed-off-by: willdavsmith --- bicep-types | 2 +- go.mod | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bicep-types b/bicep-types index 7c34fe65c7..ba8eaca5ec 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 7c34fe65c70469beda773d603805da0a6224ccc3 +Subproject commit ba8eaca5ec71d0a62089c3972f247275d0a08bd1 diff --git a/go.mod b/go.mod index eb0a485bf8..ae334ebcc5 100644 --- a/go.mod +++ b/go.mod @@ -277,8 +277,6 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect From f7ef451dfeb1f4f992829bf418dfb7c25e9d1011 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 23 Dec 2024 14:52:41 -0800 Subject: [PATCH 36/65] PR Signed-off-by: willdavsmith --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ae334ebcc5..894cfd1269 100644 --- a/go.mod +++ b/go.mod @@ -141,7 +141,6 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -277,6 +276,7 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect From e47c1f0730c7ec69004f2fffa89cec6830542b6d Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 24 Dec 2024 08:56:27 -0800 Subject: [PATCH 37/65] PR Signed-off-by: willdavsmith --- .../reconciler/deploymentresource_reconciler.go | 4 ---- .../kubernetes/noncloud/deploymenttemplate_test.go | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 2aefc39bbe..e03456a299 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -41,10 +41,6 @@ import ( corev1 "k8s.io/api/core/v1" ) -const ( - rootFileNameField = "spec.rootFileName" -) - // DeploymentResourceReconciler reconciles a DeploymentResource object. type DeploymentResourceReconciler struct { // Client is the Kubernetes client. diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index 8710f08535..15f1917eb2 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -125,8 +125,8 @@ func Test_DeploymentTemplate_Module(t *testing.T) { fileName := "module.bicep" templateFilePath := path.Join("testdata", "module", "module.json") parameters := []string{ - "name=dt-module", - "namespace=dt-ns-module", + fmt.Sprintf("name=%s", name), + fmt.Sprintf("namespace=%s", namespace), } providerConfig, err := generateDefaultProviderConfig() @@ -195,8 +195,8 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { parameters := []string{ testutil.GetBicepRecipeRegistry(), testutil.GetBicepRecipeVersion(), - "name=dt-recipe", - "namespace=dt-ns-recipe", + fmt.Sprintf("name=%s", name), + fmt.Sprintf("namespace=%s", namespace), } providerConfig, err := generateDefaultProviderConfig() From e8c861a8a290ee6319cb7bb9faf6cee182623fa3 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 24 Dec 2024 08:57:56 -0800 Subject: [PATCH 38/65] submodule Signed-off-by: willdavsmith --- bicep-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicep-types b/bicep-types index ba8eaca5ec..3676a8bf68 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit ba8eaca5ec71d0a62089c3972f247275d0a08bd1 +Subproject commit 3676a8bf689e62780c64c79bdca42f1799958cd4 From 4ed836d2d6cff24c2d3dc7d45b54f55f1ddda488 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 24 Dec 2024 09:00:05 -0800 Subject: [PATCH 39/65] PR Signed-off-by: willdavsmith --- .../deploymenttemplate/deploymenttemplate.json | 6 ++---- .../kubernetes/noncloud/testdata/env/env.json | 6 ++---- .../noncloud/testdata/module/module.json | 14 ++++---------- .../noncloud/testdata/recipe/recipe.json | 15 ++++----------- 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json index 0fbfcc324e..f1698cc4db 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -53,4 +51,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json index 8fb3e9cbea..201939108b 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/env/env.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/env/env.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -43,4 +41,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index 94c52f10fc..84cdb12106 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -65,9 +63,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -108,9 +104,7 @@ } } }, - "dependsOn": [ - "env" - ] + "dependsOn": ["env"] } } -} \ No newline at end of file +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json index df572c61cb..58bbde26da 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -65,9 +63,7 @@ "environment": "[reference('env').id]" } }, - "dependsOn": [ - "env" - ] + "dependsOn": ["env"] }, "recipe": { "import": "Radius", @@ -79,10 +75,7 @@ "environment": "[reference('env').id]" } }, - "dependsOn": [ - "app", - "env" - ] + "dependsOn": ["app", "env"] } } -} \ No newline at end of file +} From ab65f7f61b7883d31b2e62ef938851f3ff7b689c Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 24 Dec 2024 09:53:43 -0800 Subject: [PATCH 40/65] Fixing tests Signed-off-by: willdavsmith --- dt.yaml | 79 +++++++++++++++++++ .../generatekubernetesmanifest.go | 13 ++- .../deploymenttemplate.json | 6 +- .../deploymenttemplate.yaml | 1 - 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 dt.yaml diff --git a/dt.yaml b/dt.yaml new file mode 100644 index 0000000000..1af14a39e5 --- /dev/null +++ b/dt.yaml @@ -0,0 +1,79 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: deploymenttemplate.bicep +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/f" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/f" + } + } + } + rootFileName: deploymenttemplate.bicep + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "2424129636029775739", + "version": "0.31.92.45157" + } + }, + "parameters": { + "kubernetesNamespace": { + "defaultValue": "default", + "type": "string" + }, + "tag": { + "defaultValue": "latest", + "type": "string" + } + }, + "resources": { + "parameters": { + "import": "Radius", + "properties": { + "name": "parameters", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "[parameters('kubernetesNamespace')]", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('ghcr.io/myregistry:{0}', parameters('tag'))]" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + } + } + } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index eb0c7f65cc..9d55fce0d3 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -17,6 +17,7 @@ limitations under the License. package bicep import ( + "bytes" "context" "encoding/json" "fmt" @@ -37,7 +38,7 @@ import ( ) const ( - resourceGroupRequiredMessage = "ResourceGroup is required. Please provide a value for the --resource-group flag." + resourceGroupRequiredMessage = "Radius resource group is required. Please provide a value for the --group (-g) flag." ) // NewCommand creates a command for the `rad bicep generate-kubernetes-manifest` command. @@ -229,12 +230,18 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string // createDeploymentTemplateYAMLFile creates a DeploymentTemplate YAML file with the given content. func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string]any) error { - deploymentTemplateYaml, err := yaml.Marshal(deploymentTemplate) + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + + // Set the indentation to 2 spaces + encoder.SetIndent(2) + + err := encoder.Encode(deploymentTemplate) if err != nil { return err } - return r.FileSystem.WriteFile(r.DestinationFile, deploymentTemplateYaml, 0644) + return r.FileSystem.WriteFile(r.DestinationFile, buf.Bytes(), 0644) } // generateProviderConfig generates a ProviderConfig object based on the given scopes. diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json index f1698cc4db..0fbfcc324e 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json @@ -4,7 +4,9 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -51,4 +53,4 @@ } } } -} +} \ No newline at end of file diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml index 20bb06dd19..590cfe8eee 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml @@ -2,7 +2,6 @@ apiVersion: radapp.io/v1alpha3 kind: DeploymentTemplate metadata: name: deploymenttemplate.bicep - namespace: radius-system spec: parameters: tag: v1.0.0 From 0f9fd39d5bc77d31e9839b0febcad1dd6b1d8128 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Tue, 24 Dec 2024 10:14:29 -0800 Subject: [PATCH 41/65] PR Signed-off-by: willdavsmith --- .../testdata/deploymenttemplate/deploymenttemplate.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json index 0fbfcc324e..f1698cc4db 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -53,4 +51,4 @@ } } } -} \ No newline at end of file +} From a17b4bb62513d08fcaa2974ed240d7898e0d9f74 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 26 Dec 2024 09:10:43 -0800 Subject: [PATCH 42/65] namespacing Signed-off-by: willdavsmith --- .../testdata/module/module-dependency.bicep | 7 ++++ .../noncloud/testdata/module/module.bicep | 1 + .../noncloud/testdata/module/module.json | 32 +++++++++++++++---- .../noncloud/testdata/recipe/recipe.bicep | 6 ++++ .../noncloud/testdata/recipe/recipe.json | 25 +++++++++++---- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep index 6fbd41584b..0068ac1c3a 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module-dependency.bicep @@ -2,11 +2,18 @@ extension radius param name string param envId string +param namespace string resource app 'Applications.Core/applications@2023-10-01-preview' = { name: '${name}-app' properties: { environment: envId + extensions: [ + { + kind: 'kubernetesNamespace' + namespace: namespace + } + ] } } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep index 21e48b3a1a..9554e8206e 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep @@ -19,5 +19,6 @@ module module 'module-dependency.bicep' = { params: { name: name envId: env.id + namespace: namespace } } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index 84cdb12106..a4665cbcbb 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -4,11 +4,13 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", "version": "0.32.4.45862", - "templateHash": "18123004600558185514" + "templateHash": "425481402541999124" } }, "parameters": { @@ -55,6 +57,9 @@ }, "envId": { "value": "[reference('env').id]" + }, + "namespace": { + "value": "[parameters('namespace')]" } }, "template": { @@ -63,11 +68,13 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", "version": "0.32.4.45862", - "templateHash": "6618993452395382629" + "templateHash": "5602568770499112182" } }, "parameters": { @@ -76,6 +83,9 @@ }, "envId": { "type": "string" + }, + "namespace": { + "type": "string" } }, "imports": { @@ -91,7 +101,13 @@ "properties": { "name": "[format('{0}-app', parameters('name'))]", "properties": { - "environment": "[parameters('envId')]" + "environment": "[parameters('envId')]", + "extensions": [ + { + "kind": "kubernetesNamespace", + "namespace": "[parameters('namespace')]" + } + ] } } } @@ -104,7 +120,9 @@ } } }, - "dependsOn": ["env"] + "dependsOn": [ + "env" + ] } } -} +} \ No newline at end of file diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep index 2dfca85c9a..e1b683facd 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep @@ -28,6 +28,12 @@ resource app 'Applications.Core/applications@2023-10-01-preview' = { name: '${name}-app' properties: { environment: env.id + extensions: [ + { + kind: 'kubernetesNamespace' + namespace: namespace + } + ] } } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json index 58bbde26da..0da10b5ce2 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json @@ -4,11 +4,13 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], "_generator": { "name": "bicep", "version": "0.32.4.45862", - "templateHash": "11546695473811123326" + "templateHash": "428005444297831394" } }, "parameters": { @@ -60,10 +62,18 @@ "properties": { "name": "[format('{0}-app', parameters('name'))]", "properties": { - "environment": "[reference('env').id]" + "environment": "[reference('env').id]", + "extensions": [ + { + "kind": "kubernetesNamespace", + "namespace": "[parameters('namespace')]" + } + ] } }, - "dependsOn": ["env"] + "dependsOn": [ + "env" + ] }, "recipe": { "import": "Radius", @@ -75,7 +85,10 @@ "environment": "[reference('env').id]" } }, - "dependsOn": ["app", "env"] + "dependsOn": [ + "app", + "env" + ] } } -} +} \ No newline at end of file From d7bb5095ea320fa1824cbcae327d2bac8290e503 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 26 Dec 2024 09:16:27 -0800 Subject: [PATCH 43/65] fmt Signed-off-by: willdavsmith --- .../noncloud/testdata/module/module.json | 14 ++++---------- .../noncloud/testdata/recipe/recipe.json | 15 ++++----------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index a4665cbcbb..b1e8260658 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -68,9 +66,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -120,9 +116,7 @@ } } }, - "dependsOn": [ - "env" - ] + "dependsOn": ["env"] } } -} \ No newline at end of file +} diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json index 0da10b5ce2..22f56d268f 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.32.4.45862", @@ -71,9 +69,7 @@ ] } }, - "dependsOn": [ - "env" - ] + "dependsOn": ["env"] }, "recipe": { "import": "Radius", @@ -85,10 +81,7 @@ "environment": "[reference('env').id]" } }, - "dependsOn": [ - "app", - "env" - ] + "dependsOn": ["app", "env"] } } -} \ No newline at end of file +} From 667b6828ae9dc25bce7bab2b244e5d7e47722686 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 27 Dec 2024 08:06:11 -0800 Subject: [PATCH 44/65] fix test Signed-off-by: willdavsmith --- .../kubernetes/noncloud/testdata/module/module.bicep | 2 +- .../kubernetes/noncloud/testdata/module/module.json | 4 ++-- .../kubernetes/noncloud/testdata/recipe/recipe.bicep | 2 +- .../kubernetes/noncloud/testdata/recipe/recipe.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep index 9554e8206e..5b53d5f980 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.bicep @@ -9,7 +9,7 @@ resource env 'Applications.Core/environments@2023-10-01-preview' = { compute: { kind: 'kubernetes' resourceId: 'self' - namespace: namespace + namespace: '${name}-env' } } } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json index b1e8260658..2202ad01dc 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/module/module.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/module/module.json @@ -8,7 +8,7 @@ "_generator": { "name": "bicep", "version": "0.32.4.45862", - "templateHash": "425481402541999124" + "templateHash": "10911240203111091281" } }, "parameters": { @@ -35,7 +35,7 @@ "compute": { "kind": "kubernetes", "resourceId": "self", - "namespace": "[parameters('namespace')]" + "namespace": "[format('{0}-env', parameters('name'))]" } } } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep index e1b683facd..4c60fb67db 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.bicep @@ -11,7 +11,7 @@ resource env 'Applications.Core/environments@2023-10-01-preview' = { compute: { kind: 'kubernetes' resourceId: 'self' - namespace: namespace + namespace: '${name}-env' } recipes: { 'Applications.Datastores/redisCaches': { diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json index 22f56d268f..f55381232e 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.json @@ -8,7 +8,7 @@ "_generator": { "name": "bicep", "version": "0.32.4.45862", - "templateHash": "428005444297831394" + "templateHash": "11540297415417574795" } }, "parameters": { @@ -41,7 +41,7 @@ "compute": { "kind": "kubernetes", "resourceId": "self", - "namespace": "[parameters('namespace')]" + "namespace": "[format('{0}-env', parameters('name'))]" }, "recipes": { "Applications.Datastores/redisCaches": { From 483e2101e771126c18de1bf19c9c2819af4c6ef3 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 27 Dec 2024 09:37:26 -0800 Subject: [PATCH 45/65] fix test Signed-off-by: willdavsmith --- .../kubernetes/noncloud/deploymenttemplate_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index 15f1917eb2..c4867a59f0 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -251,7 +251,7 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { require.Eventually(t, func() bool { err = opts.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, deploymentTemplate) return apierrors.IsNotFound(err) - }, time.Second*60, time.Second*5, "waiting for deploymentTemplate to be deleted") + }, time.Minute*3, time.Second*5, "waiting for deploymentTemplate to be deleted") }) } From 76c6e0bfaaf25a86468915d789ed3587e5979ed3 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 10 Jan 2025 10:59:08 -0800 Subject: [PATCH 46/65] Using hash in deploymenttemplate.status Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 21 +- .../radius/radapp.io_deploymenttemplates.yaml | 32 +-- dt.yaml | 79 ------- .../generatekubernetesmanifest.go | 1 - .../deploymenttemplate.yaml | 1 - .../v1alpha3/deploymentresource_types.go | 11 +- .../v1alpha3/deploymenttemplate_types.go | 21 +- .../reconciler/deployment_reconciler.go | 1 + .../reconciler/deployment_reconciler_test.go | 2 +- .../deploymentresource_reconciler.go | 1 - .../deploymentresource_reconciler_test.go | 3 +- .../deploymenttemplate_reconciler.go | 68 +++--- .../deploymenttemplate_reconciler_test.go | 209 +++++++++++++++--- .../reconciler/recipe_reconciler_test.go | 3 +- pkg/controller/reconciler/shared_test.go | 61 +++-- .../noncloud/deploymenttemplate_test.go | 12 +- .../noncloud/testdata/recipe/recipe.yaml | 116 ++++++++++ 17 files changed, 387 insertions(+), 255 deletions(-) delete mode 100644 dt.yaml create mode 100644 test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 18f501958d..3049d6a67b 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -17,16 +17,7 @@ spec: singular: deploymentresource scope: Namespaced versions: - - additionalPrinterColumns: - - description: Status of the resource - jsonPath: .status.phrase - name: Status - type: string - - description: Name of the Bicep file that bicep build is run on - jsonPath: .status.rootFileName - name: RootFileName - type: string - name: v1alpha3 + - name: v1alpha3 schema: openAPIV3Schema: description: DeploymentResource is the Schema for the DeploymentResources @@ -58,11 +49,6 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - rootFileName: - description: |- - RootFileName is the name of the Bicep file that - `bicep build` is run on. - type: string type: object status: description: DeploymentResourceStatus defines the observed state of DeploymentResource @@ -99,11 +85,6 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - rootFileName: - description: |- - RootFileName is the name of the Bicep file that - `bicep build` is run on. - type: string type: object type: object served: true diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index 35469029f1..3df79ca1d4 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -17,16 +17,7 @@ spec: singular: deploymenttemplate scope: Namespaced versions: - - additionalPrinterColumns: - - description: Status of the resource - jsonPath: .status.phrase - name: Status - type: string - - description: Name of the Bicep file that bicep build is run on - jsonPath: .status.rootFileName - name: RootFileName - type: string - name: v1alpha3 + - name: v1alpha3 schema: openAPIV3Schema: description: DeploymentTemplate is the Schema for the deploymenttemplates @@ -60,11 +51,6 @@ spec: providerConfig: description: ProviderConfig specifies the scope for resources type: string - rootFileName: - description: |- - RootFileName is the name of the Bicep file that - `bicep build` is run on. - type: string template: description: Template is the ARM JSON manifest that defines the resources to deploy. @@ -101,27 +87,15 @@ spec: items: type: string type: array - parameters: - description: Parameters is the ARM JSON parameters for the template. - type: string phrase: description: Phrase indicates the current status of the Deployment Template. type: string - providerConfig: - description: ProviderConfig specifies the scope for resources - type: string resource: description: Resource is the resource id of the deployment. type: string - rootFileName: - description: |- - RootFileName is the name of the Bicep file that - `bicep build` is run on. - type: string - template: - description: Template is the ARM JSON manifest that defines the resources - to deploy. + statusHash: + description: StatusHash is a hash of the DeploymentTemplate's status. type: string type: object type: object diff --git a/dt.yaml b/dt.yaml deleted file mode 100644 index 1af14a39e5..0000000000 --- a/dt.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: deploymenttemplate.bicep -spec: - parameters: {} - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/f" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/f" - } - } - } - rootFileName: deploymenttemplate.bicep - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "2424129636029775739", - "version": "0.31.92.45157" - } - }, - "parameters": { - "kubernetesNamespace": { - "defaultValue": "default", - "type": "string" - }, - "tag": { - "defaultValue": "latest", - "type": "string" - } - }, - "resources": { - "parameters": { - "import": "Radius", - "properties": { - "name": "parameters", - "properties": { - "compute": { - "kind": "kubernetes", - "namespace": "[parameters('kubernetesNamespace')]", - "resourceId": "self" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "[format('ghcr.io/myregistry:{0}', parameters('tag'))]" - } - } - } - } - }, - "type": "Applications.Core/environments@2023-10-01-preview" - } - } - } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 9d55fce0d3..282f78fcf1 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -221,7 +221,6 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string "template": string(marshalledTemplate), "parameters": params, "providerConfig": string(marshalledProviderConfig), - "rootFileName": fileName, }, } diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml index 590cfe8eee..d093174678 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml @@ -20,7 +20,6 @@ spec: } } } - rootFileName: deploymenttemplate.bicep template: |- { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index bf217ea605..077d1fa667 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -27,10 +27,6 @@ type DeploymentResourceSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - - // RootFileName is the name of the Bicep file that - // `bicep build` is run on. - RootFileName string `json:"rootFileName,omitempty"` } // DeploymentResourceStatus defines the observed state of DeploymentResource @@ -41,10 +37,6 @@ type DeploymentResourceStatus struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - // RootFileName is the name of the Bicep file that - // `bicep build` is run on. - RootFileName string `json:"rootFileName,omitempty"` - // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` @@ -77,9 +69,8 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:categories={"all","radius"} // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" -// +kubebuilder:printcolumn:name="RootFileName",type="string",JSONPath=".status.rootFileName",description="Name of the Bicep file that bicep build is run on" +// +kubebuilder:resource:categories={"all","radius"} // DeploymentResource is the Schema for the DeploymentResources API type DeploymentResource struct { diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 178657d30c..8472510aa0 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -30,10 +30,6 @@ type DeploymentTemplateSpec struct { // ProviderConfig specifies the scope for resources ProviderConfig string `json:"providerConfig,omitempty"` - - // RootFileName is the name of the Bicep file that - // `bicep build` is run on. - RootFileName string `json:"rootFileName,omitempty"` } // DeploymentTemplateStatus defines the observed state of DeploymentTemplate @@ -41,18 +37,8 @@ type DeploymentTemplateStatus struct { // ObservedGeneration is the most recent generation observed for this DeploymentTemplate. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` - // Template is the ARM JSON manifest that defines the resources to deploy. - Template string `json:"template,omitempty"` - - // Parameters is the ARM JSON parameters for the template. - Parameters string `json:"parameters,omitempty"` - - // ProviderConfig specifies the scope for resources - ProviderConfig string `json:"providerConfig,omitempty"` - - // RootFileName is the name of the Bicep file that - // `bicep build` is run on. - RootFileName string `json:"rootFileName,omitempty"` + // StatusHash is a hash of the DeploymentTemplate's status. + StatusHash string `json:"statusHash,omitempty"` // Resource is the resource id of the deployment. Resource string `json:"resource,omitempty"` @@ -92,9 +78,8 @@ const ( // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:categories={"all","radius"} // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phrase",description="Status of the resource" -// +kubebuilder:printcolumn:name="RootFileName",type="string",JSONPath=".status.rootFileName",description="Name of the Bicep file that bicep build is run on" +// +kubebuilder:resource:categories={"all","radius"} // DeploymentTemplate is the Schema for the deploymenttemplates API type DeploymentTemplate struct { diff --git a/pkg/controller/reconciler/deployment_reconciler.go b/pkg/controller/reconciler/deployment_reconciler.go index 9079570c3c..7d3db59458 100644 --- a/pkg/controller/reconciler/deployment_reconciler.go +++ b/pkg/controller/reconciler/deployment_reconciler.go @@ -577,6 +577,7 @@ func (r *DeploymentReconciler) updateDeployment(ctx context.Context, deployment // Add the hash of the secret data to the Pod definition. This will force a rollout when the secrets // change. + // TODOWILLSMITH: here hash := kubernetes.HashSecretData(secret.Data) if deployment.Spec.Template.ObjectMeta.Annotations == nil { deployment.Spec.Template.ObjectMeta.Annotations = map[string]string{} diff --git a/pkg/controller/reconciler/deployment_reconciler_test.go b/pkg/controller/reconciler/deployment_reconciler_test.go index 6b5ea42b30..49f02207e5 100644 --- a/pkg/controller/reconciler/deployment_reconciler_test.go +++ b/pkg/controller/reconciler/deployment_reconciler_test.go @@ -63,7 +63,7 @@ func SetupDeploymentTest(t *testing.T) (*mockRadiusClient, client.Client) { mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Controller: crconfig.Controller{ - SkipNameValidation: boolPtr(true), + SkipNameValidation: to.Ptr(true), }, // Suppress metrics in tests to avoid conflicts. diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index e03456a299..6965d7446f 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -114,7 +114,6 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseReady deploymentResource.Status.ProviderConfig = deploymentResource.Spec.ProviderConfig - deploymentResource.Status.RootFileName = deploymentResource.Spec.RootFileName deploymentResource.Status.Id = deploymentResource.Spec.Id err = r.Client.Status().Update(ctx, &deploymentResource) if err != nil { diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index 36b29faf8c..1327bd5605 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -22,6 +22,7 @@ import ( "time" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -66,7 +67,7 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Controller: crconfig.Controller{ - SkipNameValidation: boolPtr(true), + SkipNameValidation: to.Ptr(true), }, // Suppress metrics in tests to avoid conflicts. diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index ece57923f4..ed32ff86ca 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -18,6 +18,8 @@ package reconciler import ( "context" + "crypto/sha1" + "encoding/hex" "encoding/json" "fmt" "time" @@ -125,7 +127,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Status.ProviderConfig) + scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to parse deployment scope: %w", err) } @@ -205,7 +207,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d Spec: radappiov1alpha3.DeploymentResourceSpec{ Id: outputResourceId, ProviderConfig: deploymentTemplate.Spec.ProviderConfig, - RootFileName: deploymentTemplate.Spec.RootFileName, }, } @@ -246,25 +247,23 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d } } - specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) - stringifiedSpecParameters, err := json.MarshalIndent(specParameters, "", " ") - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to marshal parameters: %w", err) - } - providerConfig := sdkclients.ProviderConfig{} err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to unmarshal providerConfig: %w", err) } + hash, err := computeHash(deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } + // If we get here, the operation was a success. Update the status and continue. // // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.OutputResources = outputResources - deploymentTemplate.Status.Template = deploymentTemplate.Spec.Template - deploymentTemplate.Status.Parameters = string(stringifiedSpecParameters) + deploymentTemplate.Status.StatusHash = hash deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name return ctrl.Result{}, nil @@ -305,9 +304,6 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl // fully processed any status changes until the async operation completes. deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation - deploymentTemplate.Status.ProviderConfig = deploymentTemplate.Spec.ProviderConfig - deploymentTemplate.Status.RootFileName = deploymentTemplate.Spec.RootFileName - updatePoller, err := r.startPutOperationIfNeeded(ctx, deploymentTemplate) if err != nil { logger.Error(err, "Unable to create or update resource.") @@ -435,24 +431,17 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con logger := ucplog.FromContextOrDiscard(ctx) specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) - stringifiedSpecParameters, err := json.MarshalIndent(specParameters, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal parameters: %w", err) - } // If the resource is already created and is up-to-date, then we don't need to do anything. - if deploymentTemplate.Status.Template == deploymentTemplate.Spec.Template && - deploymentTemplate.Status.Parameters == string(stringifiedSpecParameters) && - deploymentTemplate.Status.RootFileName == deploymentTemplate.Spec.RootFileName && - deploymentTemplate.Status.ProviderConfig == deploymentTemplate.Spec.ProviderConfig { - logger.Info("Resource is already created and is up-to-date.") + if isUpToDate(deploymentTemplate) { + logger.Info("Resource is up-to-date.") return nil, nil } - logger.Info("Template, Parameters, RootFileName, or ProviderConfig have changed, starting PUT operation.") + logger.Info("Desired state has changed, starting PUT operation.") var template any - err = json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) + err := json.Unmarshal([]byte(deploymentTemplate.Spec.Template), &template) if err != nil { return nil, fmt.Errorf("failed to unmarshal template: %w", err) } @@ -469,10 +458,10 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con return nil, fmt.Errorf("providerConfig.Deployments.Value.Scope is empty") } - // NOTE: using resource groups with lowercase here is a workaround for a casing bug in `rad app graph`. - // When https://github.com/radius-project/radius/issues/6422 is fixed we can use the more correct casing. - resourceGroupID := "/planes/radius/local/resourcegroups/default" - err = createResourceGroupIfNotExists(ctx, r.Radius, resourceGroupID) + // Create the Radius resource group corresponding the providerConfig.Deployments.Value.Scope + // if it does not exist. This is necessary because the resource group is required for the + // deployment operation. + err = createResourceGroupIfNotExists(ctx, r.Radius, providerConfig.Deployments.Value.Scope) if err != nil { return nil, fmt.Errorf("failed to create resource group: %w", err) } @@ -545,6 +534,29 @@ func isOwnedBy(resource radappiov1alpha3.DeploymentResource, owner *radappiov1al return false } +// computeHash computes a hash of the DeploymentTemplate's spec (desired state). +func computeHash(deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (string, error) { + b, err := json.Marshal(deploymentTemplate.Spec) + if err != nil { + return "", err + } + + sum := sha1.Sum(b) + hash := hex.EncodeToString(sum[:]) + return hash, nil +} + +// isUpToDate returns true if the desired state of the DeploymentTemplate +// matches the observed state. +func isUpToDate(deploymentTemplate *radappiov1alpha3.DeploymentTemplate) bool { + hash, err := computeHash(deploymentTemplate) + if err != nil { + return false + } + + return deploymentTemplate.Status.StatusHash == hash +} + // SetupWithManager sets up the controller with the Manager. func (r *DeploymentTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index f235c9ff00..c7fcb2ca67 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -27,6 +27,7 @@ import ( "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -62,7 +63,7 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Controller: crconfig.Controller{ - SkipNameValidation: boolPtr(true), + SkipNameValidation: to.Ptr(true), }, // Suppress metrics in tests to avoid conflicts. @@ -102,6 +103,124 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client return radius, mgr.GetClient() } +func Test_DeploymentTemplateReconciler_ComputeHash(t *testing.T) { + testcases := []struct { + name string + deploymentTemplate *radappiov1alpha3.DeploymentTemplate + expected string + }{ + { + name: "empty", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{}, + }, + expected: "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f", + }, + { + name: "simple", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: "{}", + Parameters: map[string]string{}, + ProviderConfig: "{}", + }, + }, + expected: "47ee899e74561942ee36a02ffd80be955e251583", + }, + { + name: "complex", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: `{"resources":[{"type":"Microsoft.Resources/deployments","apiVersion":"2020-06-01","name":"test-deploymenttemplate-basic","properties":{"mode":"Incremental","template":{},"parameters":{}}}]}`, + Parameters: map[string]string{"param1": "value1", "param2": "value2"}, + ProviderConfig: `{"AWS":{"type":"aws","value":{"scope":"scope"}}}`, + }, + }, + expected: "5c83b7122697599db2a47f2d5f7e29f4b9e3c869", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + hash, err := computeHash(tc.deploymentTemplate) + require.NoError(t, err) + require.Equal(t, tc.expected, hash) + }) + } +} + +func Test_DeploymentTemplateReconciler_IsUpToDate(t *testing.T) { + testcases := []struct { + name string + deploymentTemplate *radappiov1alpha3.DeploymentTemplate + expected bool + }{ + { + name: "up-to-date", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: "{}", + Parameters: map[string]string{}, + ProviderConfig: "{}", + }, + Status: radappiov1alpha3.DeploymentTemplateStatus{ + StatusHash: "47ee899e74561942ee36a02ffd80be955e251583", + }, + }, + expected: true, + }, + { + name: "not-up-to-date", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: "{}", + Parameters: map[string]string{}, + ProviderConfig: "{}", + }, + Status: radappiov1alpha3.DeploymentTemplateStatus{ + StatusHash: "incorrecthash", + }, + }, + expected: false, + }, + { + name: "complex-up-to-date", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: `{"resources":[{"type":"Microsoft.Resources/deployments","apiVersion":"2020-06-01","name":"test-deploymenttemplate-basic","properties":{"mode":"Incremental","template":{},"parameters":{}}}]}`, + Parameters: map[string]string{"param1": "value1", "param2": "value2"}, + ProviderConfig: `{"AWS":{"type":"aws","value":{"scope":"scope"}}}`, + }, + Status: radappiov1alpha3.DeploymentTemplateStatus{ + StatusHash: "5c83b7122697599db2a47f2d5f7e29f4b9e3c869", + }, + }, + expected: true, + }, + { + name: "complex-not-up-to-date", + deploymentTemplate: &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: `{"resources":[{"type":"Microsoft.Resources/deployments","apiVersion":"2020-06-01","name":"test-deploymenttemplate-basic","properties":{"mode":"Incremental","template":{},"parameters":{}}}]}`, + Parameters: map[string]string{"param1": "value1", "param2": "value2"}, + ProviderConfig: `{"AWS":{"type":"aws","value":{"scope":"scope"}}}`, + }, + Status: radappiov1alpha3.DeploymentTemplateStatus{ + StatusHash: "incorrecthash", + }, + }, + expected: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + isUpToDate := isUpToDate(tc.deploymentTemplate) + require.Equal(t, tc.expected, isUpToDate) + }) + } +} + func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) radius, client := SetupDeploymentTemplateTest(t) @@ -110,23 +229,22 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", generateDefaultProviderConfig(), "deploymenttemplate-basic.bicep", map[string]string{}) + scope := "/planes/radius/local/resourcegroups/deploymenttemplate-basic" + providerConfig, err := generateProviderConfig(scope, "", "") + require.NoError(t, err) + + deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) // Wait for the DeploymentTemplate to enter the updating state. status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - // Verify the provider config is parsed correctly. - scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) - require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default", scope) - radius.CompleteOperation(status.Operation.ResumeToken, nil) // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/default/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) // Verify that the Radius deployment contains the expected properties. expectedProperties := map[string]any{ @@ -137,26 +255,34 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/default", + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-basic", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/default", + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-basic", }, }, }, } - resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + resource, err := radius.Resources("/planes/radius/local/resourcegroups/deploymenttemplate-basic", "Microsoft.Resources/deployments").Get(ctx, name.Name) require.NoError(t, err) require.Equal(t, expectedProperties, resource.Properties) // Verify that the DeploymentTemplate contains the expected properties. - require.Equal(t, "{}", status.Template) - require.Equal(t, "{}", status.Parameters) - require.Equal(t, string(generateDefaultProviderConfig()), status.ProviderConfig) - require.Equal(t, "deploymenttemplate-basic.bicep", status.RootFileName) + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: "{}", + Parameters: map[string]string{}, + ProviderConfig: providerConfig, + }, + } + + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + + require.Equal(t, expectedStatusHash, status.StatusHash) // Delete the DeploymentTemplate err = client.Delete(ctx, deploymentTemplate) @@ -179,7 +305,11 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", generateDefaultProviderConfig(), "deploymenttemplate-failurerecovery.bicep", map[string]string{}) + scope := "/planes/radius/local/resourcegroups/deploymenttemplate-failurerecovery" + providerConfig, err := generateProviderConfig(scope, "", "") + require.NoError(t, err) + + deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -227,37 +357,36 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { template, err := json.MarshalIndent(templateMap, "", " ") require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, string(template), generateDefaultProviderConfig(), "deploymenttemplate-withresources.bicep", map[string]string{}) + scope := "/planes/radius/local/resourcegroups/deploymenttemplate-withresources" + providerConfig, err := generateProviderConfig(scope, "", "") + require.NoError(t, err) + + deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - // Verify the provider config is parsed correctly. - scope, err := ParseDeploymentScopeFromProviderConfig(status.ProviderConfig) - require.NoError(t, err) - require.Equal(t, "/planes/radius/local/resourcegroups/default", scope) - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { resource, ok := radius.resources[state.resourceID] require.True(t, ok, "failed to find resource") resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env"}, + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env"}, } state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/default/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) // DeploymentTemplate will be waiting for environment to be created. - createEnvironment(radius, "default", "env") + createEnvironment(radius, "deploymenttemplate-withresources", "env") dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "env"} dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env", dependencyStatus.Id) // Verify that the Radius deployment contains the expected properties. resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) @@ -270,37 +399,45 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/default", + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-withresources", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/default", + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-withresources", }, }, }, "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/env"}, + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env"}, }, } require.Equal(t, expectedProperties, resource.Properties) // Verify that the DeploymentTemplate contains the expected properties. - require.Equal(t, string(template), status.Template) - require.Equal(t, "{}", status.Parameters) - require.Equal(t, string(generateDefaultProviderConfig()), status.ProviderConfig) - require.Equal(t, "deploymenttemplate-withresources.bicep", status.RootFileName) + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: string(template), + Parameters: map[string]string{}, + ProviderConfig: providerConfig, + }, + } + + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + + require.Equal(t, expectedStatusHash, status.StatusHash) err = client.Delete(ctx, deploymentTemplate) require.NoError(t, err) - waitForDeploymentTemplateStateDeleting(t, client, name, nil) + waitForDeploymentTemplateStateDeleting(t, client, name) dependencyStatus = waitForDeploymentResourceStateDeleting(t, client, dependencyName, nil) // Delete the environment. - deleteEnvironment(radius, "default", "env") + deleteEnvironment(radius, "deploymenttemplate-withresources", "env") // Complete the delete operation on the DeploymentResource. radius.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) @@ -361,7 +498,7 @@ func waitForDeploymentTemplateStateReady(t *testing.T, client client.Client, nam return status } -func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { +func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) logger := t diff --git a/pkg/controller/reconciler/recipe_reconciler_test.go b/pkg/controller/reconciler/recipe_reconciler_test.go index ed2a91bf5d..d8a3dadff4 100644 --- a/pkg/controller/reconciler/recipe_reconciler_test.go +++ b/pkg/controller/reconciler/recipe_reconciler_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/radius-project/radius/pkg/cli/clients_new/generated" + "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -49,7 +50,7 @@ func SetupRecipeTest(t *testing.T) (*mockRadiusClient, client.Client) { mgr, err := ctrl.NewManager(config, ctrl.Options{ Scheme: scheme, Controller: crconfig.Controller{ - SkipNameValidation: boolPtr(true), + SkipNameValidation: to.Ptr(true), }, // Suppress metrics in tests to avoid conflicts. diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index 4c83fc9a37..91e50f3697 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -17,6 +17,7 @@ limitations under the License. package reconciler import ( + "encoding/json" "fmt" "testing" "time" @@ -24,6 +25,7 @@ import ( v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" @@ -195,11 +197,7 @@ func makeDeployment(name types.NamespacedName) *appsv1.Deployment { } } -func boolPtr(b bool) *bool { - return &b -} - -func makeDeploymentTemplate(name types.NamespacedName, template string, providerConfig string, rootFileName string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { +func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { return &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: ctrl.ObjectMeta{ Namespace: name.Namespace, @@ -208,7 +206,6 @@ func makeDeploymentTemplate(name types.NamespacedName, template string, provider Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: template, ProviderConfig: providerConfig, - RootFileName: rootFileName, Parameters: parameters, }, } @@ -226,21 +223,43 @@ func makeDeploymentResource(name types.NamespacedName, id string) *radappiov1alp } } -func generateDefaultProviderConfig() string { - return ` - { - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourcegroups/default" - } +func generateProviderConfig(radiusScope, azureScope, awsScope string) (string, error) { + if radiusScope == "" { + return "", fmt.Errorf("radiusScope is required") + } + + providerConfig := sdkclients.ProviderConfig{} + if awsScope != "" { + providerConfig.AWS = &sdkclients.AWS{ + Type: "aws", + Value: sdkclients.Value{ + Scope: awsScope, }, - "radius": { - "type": "Radius", - "value": { - "scope": "/planes/radius/local/resourcegroups/default" - } - } } - ` + } + if azureScope != "" { + providerConfig.Az = &sdkclients.Az{ + Type: "azure", + Value: sdkclients.Value{ + Scope: azureScope, + }, + } + } + + providerConfig.Radius = &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: radiusScope, + }, + } + providerConfig.Deployments = &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: radiusScope, + }, + } + + b, err := json.Marshal(providerConfig) + + return string(b), err } diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index c4867a59f0..3ffa49e1df 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -55,7 +55,6 @@ func Test_DeploymentTemplate_Basic(t *testing.T) { name := "dt-env" namespace := "dt-env-ns" - fileName := "env.bicep" templateFilePath := path.Join("testdata", "env", "env.json") parameters := []string{ fmt.Sprintf("name=%s", name), @@ -74,7 +73,7 @@ func Test_DeploymentTemplate_Basic(t *testing.T) { _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) - deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, parametersMap) t.Run("Create DeploymentTemplate", func(t *testing.T) { t.Log("Creating DeploymentTemplate") @@ -122,7 +121,6 @@ func Test_DeploymentTemplate_Module(t *testing.T) { name := "dt-module" namespace := "dt-module-ns" - fileName := "module.bicep" templateFilePath := path.Join("testdata", "module", "module.json") parameters := []string{ fmt.Sprintf("name=%s", name), @@ -141,7 +139,7 @@ func Test_DeploymentTemplate_Module(t *testing.T) { _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) - deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, parametersMap) t.Run("Create DeploymentTemplate", func(t *testing.T) { t.Log("Creating DeploymentTemplate") @@ -190,7 +188,6 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { name := "dt-recipe" namespace := "dt-recipe-ns" - fileName := "recipe.bicep" templateFilePath := path.Join("testdata", "recipe", "recipe.json") parameters := []string{ testutil.GetBicepRecipeRegistry(), @@ -211,7 +208,7 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { _, err = opts.K8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, metav1.CreateOptions{}) require.NoError(t, controller_runtime.IgnoreAlreadyExists(err)) - deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, fileName, parametersMap) + deploymentTemplate := makeDeploymentTemplate(types.NamespacedName{Name: name, Namespace: namespace}, string(template), providerConfig, parametersMap) t.Run("Create DeploymentTemplate", func(t *testing.T) { t.Log("Creating DeploymentTemplate") @@ -255,7 +252,7 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { }) } -func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig, rootFileName string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { +func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: name.Name, @@ -265,7 +262,6 @@ func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig, Template: template, Parameters: parameters, ProviderConfig: providerConfig, - RootFileName: rootFileName, }, } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml new file mode 100644 index 0000000000..302e5cd9b3 --- /dev/null +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml @@ -0,0 +1,116 @@ +apiVersion: radapp.io/v1alpha3 +kind: DeploymentTemplate +metadata: + name: recipe.bicep +spec: + parameters: {} + providerConfig: |- + { + "radius": { + "type": "radius", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + }, + "deployments": { + "type": "Microsoft.Resources", + "value": { + "scope": "/planes/radius/local/resourceGroups/default" + } + } + } + template: |- + { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "languageVersion": "2.1-experimental", + "metadata": { + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_generator": { + "name": "bicep", + "templateHash": "7805117482855564", + "version": "0.31.92.45157" + } + }, + "parameters": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "resources": { + "app": { + "dependsOn": [ + "env" + ], + "import": "Radius", + "properties": { + "name": "[format('{0}-app', parameters('name'))]", + "properties": { + "environment": "[reference('env').id]", + "extensions": [ + { + "kind": "kubernetesNamespace", + "namespace": "[parameters('namespace')]" + } + ] + } + }, + "type": "Applications.Core/applications@2023-10-01-preview" + }, + "env": { + "import": "Radius", + "properties": { + "name": "[format('{0}-env', parameters('name'))]", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "[format('{0}-env', parameters('name'))]", + "resourceId": "self" + }, + "recipes": { + "Applications.Datastores/redisCaches": { + "default": { + "templateKind": "bicep", + "templatePath": "[format('{0}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:{1}', parameters('registry'), parameters('version'))]" + } + } + } + } + }, + "type": "Applications.Core/environments@2023-10-01-preview" + }, + "recipe": { + "dependsOn": [ + "app", + "env" + ], + "import": "Radius", + "properties": { + "name": "[format('{0}-recipe', parameters('name'))]", + "properties": { + "application": "[reference('app').id]", + "environment": "[reference('env').id]" + } + }, + "type": "Applications.Datastores/redisCaches@2023-10-01-preview" + } + } + } From abf49af9c652a1d21b08d0e869e58d406785587f Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 13 Jan 2025 14:39:46 -0800 Subject: [PATCH 47/65] fix test Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 7 ++- .../radius/radapp.io_deploymenttemplates.yaml | 7 ++- .../deploymentresource_reconciler.go | 45 +++++++++---------- .../noncloud/testdata/recipe/recipe.yaml | 14 +++--- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 3049d6a67b..9d690137bc 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -17,7 +17,12 @@ spec: singular: deploymentresource scope: Namespaced versions: - - name: v1alpha3 + - additionalPrinterColumns: + - description: Status of the resource + jsonPath: .status.phrase + name: Status + type: string + name: v1alpha3 schema: openAPIV3Schema: description: DeploymentResource is the Schema for the DeploymentResources diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index 3df79ca1d4..81a6d66c07 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -17,7 +17,12 @@ spec: singular: deploymenttemplate scope: Namespaced versions: - - name: v1alpha3 + - additionalPrinterColumns: + - description: Status of the resource + jsonPath: .status.phrase + name: Status + type: string + name: v1alpha3 schema: openAPIV3Schema: description: DeploymentTemplate is the Schema for the deploymenttemplates diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 6965d7446f..0667368799 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -106,7 +106,7 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R } } - if deploymentResource.DeletionTimestamp != nil { + if deploymentResource.ObjectMeta.DeletionTimestamp != nil { return r.reconcileDelete(ctx, &deploymentResource) } @@ -365,33 +365,30 @@ func checkForDeploymentResourceDependencies(deploymentResource *radappiov1alpha3 return "", err } - if strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/applications") { - return "", nil - } - - if strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/environments") { - return "", nil - } + // If the deploymentResource is an application or environment, check if other resources exist + if strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/applications") || strings.EqualFold(deploymentResourceID.Type(), "Applications.Core/environments") { + resourceCount := 0 + dependentResource := "" + for _, dr := range deploymentResourceList { + if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { + continue + } - resourceCount := 0 - dependentResource := "" - for _, dr := range deploymentResourceList { - // shouldn't need this... - // if dr.Status.Phrase == radappiov1alpha3.DeploymentResourcePhraseDeleted { - // continue - // } + id, err := resources.ParseResource(dr.Spec.Id) + if err != nil { + return "", err + } - id, err := resources.ParseResource(dr.Spec.Id) - if err != nil { - return "", err + // don't count applications or environments + if !strings.EqualFold(id.Type(), "Applications.Core/applications") && !strings.EqualFold(id.Type(), "Applications.Core/environments") { + resourceCount++ + dependentResource = dr.Spec.Id + } } - // don't count applications or environments - if !strings.EqualFold(id.Type(), "Applications.Core/applications") && !strings.EqualFold(id.Type(), "Applications.Core/environments") { - resourceCount++ - dependentResource = dr.Spec.Id - } + return dependentResource, nil } - return dependentResource, nil + // If the deploymentResource is not an application or environment, just return + return "", nil } diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml index 302e5cd9b3..d1fb0d9072 100644 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml +++ b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml @@ -3,19 +3,23 @@ kind: DeploymentTemplate metadata: name: recipe.bicep spec: - parameters: {} + parameters: + name: dt-recipe + namespace: dt-recipe + registry: ghcr.io/radius-project/dev + version: pr-funcf74af565f0 providerConfig: |- { "radius": { "type": "radius", "value": { - "scope": "/planes/radius/local/resourceGroups/default" + "scope": "/planes/radius/local/resourceGroups/jambalaya" } }, "deployments": { "type": "Microsoft.Resources", "value": { - "scope": "/planes/radius/local/resourceGroups/default" + "scope": "/planes/radius/local/resourceGroups/jambalaya" } } } @@ -37,8 +41,8 @@ spec: "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", "_generator": { "name": "bicep", - "templateHash": "7805117482855564", - "version": "0.31.92.45157" + "templateHash": "11540297415417574795", + "version": "0.32.4.45862" } }, "parameters": { From 5d7fa571c6e9452910c5eed6d5dc31f7a9183cc8 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 13 Jan 2025 15:18:34 -0800 Subject: [PATCH 48/65] PR Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 3 - .../v1alpha3/deploymentresource_types.go | 3 - .../deploymentresource_reconciler.go | 3 +- .../noncloud/testdata/recipe/recipe.yaml | 120 ------------------ 4 files changed, 1 insertion(+), 128 deletions(-) delete mode 100644 test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index 9d690137bc..c042c2c09b 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -87,9 +87,6 @@ spec: description: Phrase indicates the current status of the Deployment Resource. type: string - providerConfig: - description: ProviderConfig specifies the scope for resources - type: string type: object type: object served: true diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index 077d1fa667..e2e7fbc60c 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -34,9 +34,6 @@ type DeploymentResourceStatus struct { // Id is the resource Id. Id string `json:"id,omitempty"` - // ProviderConfig specifies the scope for resources - ProviderConfig string `json:"providerConfig,omitempty"` - // ObservedGeneration is the most recent generation observed for this DeploymentResource. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 0667368799..1e69c049a7 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -113,7 +113,6 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R logger.Info("Resource is in desired state.", "resourceId", deploymentResource.Spec.Id) deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseReady - deploymentResource.Status.ProviderConfig = deploymentResource.Spec.ProviderConfig deploymentResource.Status.Id = deploymentResource.Spec.Id err = r.Client.Status().Update(ctx, &deploymentResource) if err != nil { @@ -355,7 +354,7 @@ func listResourcesWithSameOwner(ctx context.Context, c client.Client, namespace // checkForDeploymentResourceDependencies checks if the deploymentResource is an application or environment. // If it is, it checks if other (non-application or environment) resources exist. // If other resources exist, it returns the ID of one of the dependent resources. -// NOTE: This is a workaround for Radius API behavior. Since deleting +// NOTE: This is a workaround for existing Radius API behavior. Since deleting // an application or environment can leave hanging resources, we need to make sure to // delete these resources before deleting the application or environment. // https://github.com/radius-project/radius/issues/8164 diff --git a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml b/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml deleted file mode 100644 index d1fb0d9072..0000000000 --- a/test/functional-portable/kubernetes/noncloud/testdata/recipe/recipe.yaml +++ /dev/null @@ -1,120 +0,0 @@ -apiVersion: radapp.io/v1alpha3 -kind: DeploymentTemplate -metadata: - name: recipe.bicep -spec: - parameters: - name: dt-recipe - namespace: dt-recipe - registry: ghcr.io/radius-project/dev - version: pr-funcf74af565f0 - providerConfig: |- - { - "radius": { - "type": "radius", - "value": { - "scope": "/planes/radius/local/resourceGroups/jambalaya" - } - }, - "deployments": { - "type": "Microsoft.Resources", - "value": { - "scope": "/planes/radius/local/resourceGroups/jambalaya" - } - } - } - template: |- - { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "imports": { - "Radius": { - "provider": "Radius", - "version": "latest" - } - }, - "languageVersion": "2.1-experimental", - "metadata": { - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], - "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_generator": { - "name": "bicep", - "templateHash": "11540297415417574795", - "version": "0.32.4.45862" - } - }, - "parameters": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "registry": { - "type": "string" - }, - "version": { - "type": "string" - } - }, - "resources": { - "app": { - "dependsOn": [ - "env" - ], - "import": "Radius", - "properties": { - "name": "[format('{0}-app', parameters('name'))]", - "properties": { - "environment": "[reference('env').id]", - "extensions": [ - { - "kind": "kubernetesNamespace", - "namespace": "[parameters('namespace')]" - } - ] - } - }, - "type": "Applications.Core/applications@2023-10-01-preview" - }, - "env": { - "import": "Radius", - "properties": { - "name": "[format('{0}-env', parameters('name'))]", - "properties": { - "compute": { - "kind": "kubernetes", - "namespace": "[format('{0}-env', parameters('name'))]", - "resourceId": "self" - }, - "recipes": { - "Applications.Datastores/redisCaches": { - "default": { - "templateKind": "bicep", - "templatePath": "[format('{0}/test/testrecipes/test-bicep-recipes/redis-recipe-value-backed:{1}', parameters('registry'), parameters('version'))]" - } - } - } - } - }, - "type": "Applications.Core/environments@2023-10-01-preview" - }, - "recipe": { - "dependsOn": [ - "app", - "env" - ], - "import": "Radius", - "properties": { - "name": "[format('{0}-recipe', parameters('name'))]", - "properties": { - "application": "[reference('app').id]", - "environment": "[reference('env').id]" - } - }, - "type": "Applications.Datastores/redisCaches@2023-10-01-preview" - } - } - } From 5585544eed07ab5a44fd3f3b612b6975a3e23299 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 13 Jan 2025 15:19:08 -0800 Subject: [PATCH 49/65] PR Signed-off-by: willdavsmith --- pkg/controller/reconciler/deployment_reconciler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/controller/reconciler/deployment_reconciler.go b/pkg/controller/reconciler/deployment_reconciler.go index 7d3db59458..9079570c3c 100644 --- a/pkg/controller/reconciler/deployment_reconciler.go +++ b/pkg/controller/reconciler/deployment_reconciler.go @@ -577,7 +577,6 @@ func (r *DeploymentReconciler) updateDeployment(ctx context.Context, deployment // Add the hash of the secret data to the Pod definition. This will force a rollout when the secrets // change. - // TODOWILLSMITH: here hash := kubernetes.HashSecretData(secret.Data) if deployment.Spec.Template.ObjectMeta.Annotations == nil { deployment.Spec.Template.ObjectMeta.Annotations = map[string]string{} From 1fb419b3b747378810d500571f236f969f7c30ee Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 13 Jan 2025 15:56:41 -0800 Subject: [PATCH 50/65] re-run Signed-off-by: willdavsmith From 876ea07671b18506ecf316d73da9f7f5d4255287 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 13 Jan 2025 15:56:58 -0800 Subject: [PATCH 51/65] re-run Signed-off-by: willdavsmith From 2840562a0113b32f37c3916bf90ce50dde67483a Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 23 Jan 2025 16:50:01 -0800 Subject: [PATCH 52/65] PR Signed-off-by: willdavsmith --- .../generatekubernetesmanifest.go | 44 +--------------- pkg/sdk/clients/providerconfig.go | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 282f78fcf1..06556a1a78 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -20,7 +20,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "path/filepath" "strings" @@ -199,7 +198,7 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string return nil, err } - providerConfig := r.generateProviderConfig() + providerConfig := sdkclients.GenerateProviderConfig(r.Group, r.AWSScope, r.AzureScope) marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") if err != nil { @@ -242,44 +241,3 @@ func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string] return r.FileSystem.WriteFile(r.DestinationFile, buf.Bytes(), 0644) } - -// generateProviderConfig generates a ProviderConfig object based on the given scopes. -func (r *Runner) generateProviderConfig() (providerConfig sdkclients.ProviderConfig) { - providerConfig = sdkclients.ProviderConfig{} - if r.AWSScope != "" { - providerConfig.AWS = &sdkclients.AWS{ - Type: "aws", - Value: sdkclients.Value{ - Scope: r.AWSScope, - }, - } - } - if r.AzureScope != "" { - providerConfig.Az = &sdkclients.Az{ - Type: "azure", - Value: sdkclients.Value{ - Scope: r.AzureScope, - }, - } - } - if r.Group != "" { - providerConfig.Radius = &sdkclients.Radius{ - Type: "radius", - Value: sdkclients.Value{ - Scope: constructRadiusDeploymentScope(r.Group), - }, - } - providerConfig.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: constructRadiusDeploymentScope(r.Group), - }, - } - } - - return providerConfig -} - -func constructRadiusDeploymentScope(group string) string { - return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) -} diff --git a/pkg/sdk/clients/providerconfig.go b/pkg/sdk/clients/providerconfig.go index 8d8a810aa0..3fffee5637 100644 --- a/pkg/sdk/clients/providerconfig.go +++ b/pkg/sdk/clients/providerconfig.go @@ -16,6 +16,8 @@ limitations under the License. package clients +import "fmt" + const ( // ProviderTypeAzure is used to specify the provider configuration for Azure resources. ProviderTypeAzure = "AzureResourceManager" @@ -33,18 +35,60 @@ const ( func NewDefaultProviderConfig(resourceGroup string) ProviderConfig { config := ProviderConfig{ Deployments: &Deployments{ - Type: "Microsoft.Resources", + Type: ProviderTypeDeployments, Value: Value{ - Scope: "/planes/radius/local/resourceGroups/" + resourceGroup, + Scope: constructRadiusDeploymentScope(resourceGroup), }, }, Radius: &Radius{ - Type: "Radius", + Type: ProviderTypeRadius, Value: Value{ - Scope: "/planes/radius/local/resourceGroups/" + resourceGroup, + Scope: constructRadiusDeploymentScope(resourceGroup), }, }, } return config } + +// GenerateProviderConfig generates a ProviderConfig object based on the given scopes. +func GenerateProviderConfig(resourceGroup, awsScope, azureScope string) ProviderConfig { + providerConfig := ProviderConfig{} + if awsScope != "" { + providerConfig.AWS = &AWS{ + Type: ProviderTypeAWS, + Value: Value{ + Scope: awsScope, + }, + } + } + if azureScope != "" { + providerConfig.Az = &Az{ + Type: ProviderTypeAzure, + Value: Value{ + Scope: azureScope, + }, + } + } + if resourceGroup != "" { + providerConfig.Radius = &Radius{ + Type: ProviderTypeRadius, + Value: Value{ + Scope: constructRadiusDeploymentScope(resourceGroup), + }, + } + providerConfig.Deployments = &Deployments{ + Type: ProviderTypeDeployments, + Value: Value{ + Scope: constructRadiusDeploymentScope(resourceGroup), + }, + } + } + + return providerConfig +} + +// constructRadiusDeploymentScope constructs the scope for Radius deployments. +func constructRadiusDeploymentScope(group string) string { + return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) +} From fe357646f12d8c50cde0fe15f9309349b2d0bed6 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 23 Jan 2025 17:54:04 -0800 Subject: [PATCH 53/65] PR Signed-off-by: willdavsmith --- .../generatekubernetesmanifest.go | 12 +- .../generatekubernetesmanifest_test.go | 5 + pkg/cli/filesystem/filesystem.go | 16 ++ pkg/cli/filesystem/memmapfs.go | 16 ++ pkg/cli/filesystem/memmapfs_test.go | 16 ++ pkg/cli/filesystem/osfs.go | 16 ++ .../deploymenttemplate_reconciler_test.go | 212 ++++++++++++++++-- pkg/controller/reconciler/shared_test.go | 43 ---- .../testdata/deploymenttemplate-update-1.json | 37 +++ .../testdata/deploymenttemplate-update-2.json | 37 +++ .../deploymenttemplate-withresources.json | 4 +- pkg/sdk/clients/providerconfig.go | 2 +- .../noncloud/deploymenttemplate_test.go | 4 +- 13 files changed, 350 insertions(+), 70 deletions(-) create mode 100644 pkg/controller/reconciler/testdata/deploymenttemplate-update-1.json create mode 100644 pkg/controller/reconciler/testdata/deploymenttemplate-update-2.json diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 06556a1a78..9cf716106c 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -75,8 +75,8 @@ rad bicep generate-kubernetes-manifest app.bicep --parameters @app.bicepparam -- commonflags.AddResourceGroupFlag(cmd) commonflags.AddParameterFlag(cmd) - cmd.Flags().StringP("destination-file", "d", "", "Path of the generated DeploymentTemplate yaml file.") - _ = cmd.MarkFlagFilename("destination-file", ".yaml") + cmd.Flags().StringP("destination-file", "d", "", "Path of the generated DeploymentTemplate yaml file created by running this command.") + _ = cmd.MarkFlagFilename("destination-file", ".yaml", ".yml") cmd.Flags().String("azure-scope", "", "Scope for Azure deployment.") cmd.Flags().String("aws-scope", "", "Scope for AWS deployment.") @@ -101,7 +101,7 @@ type Runner struct { AWSScope string } -// NewRunner creates a new instance of the `rad deploy` runner. +// NewRunner creates a new instance of the `rad bicep generate-kubernetes-manifest` runner. func NewRunner(factory framework.Factory) *Runner { return &Runner{ Bicep: factory.GetBicep(), @@ -146,8 +146,8 @@ func (r *Runner) Validate(cmd *cobra.Command, args []string) error { r.DestinationFile = strings.TrimSuffix(filepath.Base(r.FilePath), filepath.Ext(r.FilePath)) + ".yaml" } - if filepath.Ext(r.DestinationFile) != ".yaml" { - return clierrors.Message("Destination file must have a .yaml extension") + if filepath.Ext(r.DestinationFile) != ".yaml" && filepath.Ext(r.DestinationFile) != ".yml" { + return clierrors.Message("Destination file must have a .yaml or .yml extension") } parameterArgs, err := cmd.Flags().GetStringArray("parameters") @@ -185,7 +185,6 @@ func (r *Runner) Run(ctx context.Context) error { return err } - // Print the path to the file r.Output.LogInfo("DeploymentTemplate file created at %s", r.DestinationFile) return nil @@ -231,7 +230,6 @@ func (r *Runner) createDeploymentTemplateYAMLFile(deploymentTemplate map[string] var buf bytes.Buffer encoder := yaml.NewEncoder(&buf) - // Set the indentation to 2 spaces encoder.SetIndent(2) err := encoder.Encode(deploymentTemplate) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go index 5d8e8d14af..9a898dae5b 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest_test.go @@ -58,6 +58,11 @@ func Test_Validate(t *testing.T) { require.Equal(t, "default", runner.Group) }, }, + { + Name: "rad bicep generate-kubernetes-manifest - invalid with no group provided", + Input: []string{"app.bicep"}, + ExpectedValid: false, + }, { Name: "rad bicep generate-kubernetes-manifest - valid with parameters", Input: []string{"app.bicep", "-g", "default", "-p", "foo=bar", "--parameters", "a=b", "--parameters", "@testdata/parameters.json"}, diff --git a/pkg/cli/filesystem/filesystem.go b/pkg/cli/filesystem/filesystem.go index aedb40b1e2..1b40ecd88c 100644 --- a/pkg/cli/filesystem/filesystem.go +++ b/pkg/cli/filesystem/filesystem.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package filesystem import ( diff --git a/pkg/cli/filesystem/memmapfs.go b/pkg/cli/filesystem/memmapfs.go index 9bf9e329bc..32572eb1ae 100644 --- a/pkg/cli/filesystem/memmapfs.go +++ b/pkg/cli/filesystem/memmapfs.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package filesystem import ( diff --git a/pkg/cli/filesystem/memmapfs_test.go b/pkg/cli/filesystem/memmapfs_test.go index c48539208b..87adc9cb31 100644 --- a/pkg/cli/filesystem/memmapfs_test.go +++ b/pkg/cli/filesystem/memmapfs_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package filesystem import ( diff --git a/pkg/cli/filesystem/osfs.go b/pkg/cli/filesystem/osfs.go index 3647e17844..771c9fba0f 100644 --- a/pkg/cli/filesystem/osfs.go +++ b/pkg/cli/filesystem/osfs.go @@ -1,3 +1,19 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package filesystem import ( diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index c7fcb2ca67..6b8bd2e86b 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -229,11 +229,12 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - scope := "/planes/radius/local/resourcegroups/deploymenttemplate-basic" - providerConfig, err := generateProviderConfig(scope, "", "") + providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-basic", "", "") + require.NoError(t, err) + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, "{}", string(marshalledProviderConfig), map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -275,7 +276,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: "{}", Parameters: map[string]string{}, - ProviderConfig: providerConfig, + ProviderConfig: string(marshalledProviderConfig), }, } @@ -305,11 +306,12 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - scope := "/planes/radius/local/resourcegroups/deploymenttemplate-failurerecovery" - providerConfig, err := generateProviderConfig(scope, "", "") + providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-failurerecovery", "", "") + require.NoError(t, err) + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, "{}", string(marshalledProviderConfig), map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -358,10 +360,12 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { require.NoError(t, err) scope := "/planes/radius/local/resourcegroups/deploymenttemplate-withresources" - providerConfig, err := generateProviderConfig(scope, "", "") + providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "") + require.NoError(t, err) + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, string(template), string(marshalledProviderConfig), map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -372,7 +376,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { require.True(t, ok, "failed to find resource") resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env"}, + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, } state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) @@ -382,11 +386,11 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) // DeploymentTemplate will be waiting for environment to be created. - createEnvironment(radius, "deploymenttemplate-withresources", "env") + createEnvironment(radius, "deploymenttemplate-withresources", "deploymenttemplate-withresources-env") - dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "env"} + dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-withresources-env"} dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) // Verify that the Radius deployment contains the expected properties. resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) @@ -410,7 +414,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { }, }, "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/env"}, + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, }, } require.Equal(t, expectedProperties, resource.Properties) @@ -420,7 +424,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: string(template), Parameters: map[string]string{}, - ProviderConfig: providerConfig, + ProviderConfig: string(marshalledProviderConfig), }, } @@ -437,7 +441,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { dependencyStatus = waitForDeploymentResourceStateDeleting(t, client, dependencyName, nil) // Delete the environment. - deleteEnvironment(radius, "deploymenttemplate-withresources", "env") + deleteEnvironment(radius, "deploymenttemplate-withresources", "deploymenttemplate-withresources-env") // Complete the delete operation on the DeploymentResource. radius.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) @@ -446,6 +450,182 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { waitForDeploymentTemplateStateDeleted(t, client, name) } +func Test_DeploymentTemplateReconciler_Update(t *testing.T) { + // This test tests our ability to update a DeploymentTemplate. + // We create a DeploymentTemplate, update it, and verify that the Radius resource is updated accordingly. + + ctx := testcontext.New(t) + radius, client := SetupDeploymentTemplateTest(t) + + name := types.NamespacedName{Namespace: "deploymenttemplate-update", Name: "test-deploymenttemplate-update"} + err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + require.NoError(t, err) + + fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-update-1.json")) + require.NoError(t, err) + templateMap := map[string]any{} + err = json.Unmarshal(fileContent, &templateMap) + require.NoError(t, err) + template, err := json.MarshalIndent(templateMap, "", " ") + require.NoError(t, err) + + scope := "/planes/radius/local/resourcegroups/deploymenttemplate-update" + providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "") + require.NoError(t, err) + marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + require.NoError(t, err) + + deploymentTemplate := makeDeploymentTemplate(name, string(template), string(marshalledProviderConfig), map[string]string{}) + err = client.Create(ctx, deploymentTemplate) + require.NoError(t, err) + + status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + + radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := radius.resources[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties["outputResources"] = []any{ + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + } + state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + }) + + // DeploymentTemplate should be ready after the operation completes. + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) + + // DeploymentTemplate will be waiting for environment to be created. + createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") + + dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} + dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + + // Verify that the Radius deployment contains the expected properties. + resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) + expectedProperties := map[string]any{ + "mode": "Incremental", + "template": templateMap, + "parameters": map[string]map[string]string{}, + "providerConfig": sdkclients.ProviderConfig{ + Radius: &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + }, + }, + Deployments: &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + }, + }, + }, + "outputResources": []any{ + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + }, + } + require.Equal(t, expectedProperties, resource.Properties) + + // Verify that the DeploymentTemplate contains the expected properties. + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: string(template), + Parameters: map[string]string{}, + ProviderConfig: string(marshalledProviderConfig), + }, + } + + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Re-deploy the DeploymentTemplate with a new template. + + fileContent, err = os.ReadFile(path.Join("testdata", "deploymenttemplate-update-2.json")) + require.NoError(t, err) + templateMap = map[string]any{} + err = json.Unmarshal(fileContent, &templateMap) + require.NoError(t, err) + template, err = json.MarshalIndent(templateMap, "", " ") + require.NoError(t, err) + + newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} + err = client.Get(ctx, name, &newDeploymentTemplate) + require.NoError(t, err) + + // Update the template + newDeploymentTemplate.Spec.Template = string(template) + + err = client.Update(ctx, &newDeploymentTemplate) + require.NoError(t, err) + + status = waitForDeploymentTemplateStateUpdating(t, client, name, nil) + + radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := radius.resources[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties["outputResources"] = []any{ + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + } + state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + }) + + // DeploymentTemplate should be ready after the operation completes. + status = waitForDeploymentTemplateStateReady(t, client, name) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) + + // DeploymentTemplate will be waiting for environment to be created. + createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") + + dependencyName = types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} + dependencyStatus = waitForDeploymentResourceStateReady(t, client, dependencyName) + require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + + // Verify that the Radius deployment contains the expected properties. + resource, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) + require.NoError(t, err) + expectedProperties = map[string]any{ + "mode": "Incremental", + "template": templateMap, + "parameters": map[string]map[string]string{}, + "providerConfig": sdkclients.ProviderConfig{ + Radius: &sdkclients.Radius{ + Type: "Radius", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + }, + }, + Deployments: &sdkclients.Deployments{ + Type: "Microsoft.Resources", + Value: sdkclients.Value{ + Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + }, + }, + }, + "outputResources": []any{ + map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + }, + } + require.Equal(t, expectedProperties, resource.Properties) + + // Verify that the DeploymentTemplate contains the expected properties. + expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: string(template), + Parameters: map[string]string{}, + ProviderConfig: string(marshalledProviderConfig), + }, + } + + expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) +} + func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index 91e50f3697..c680fca5d3 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -17,7 +17,6 @@ limitations under the License. package reconciler import ( - "encoding/json" "fmt" "testing" "time" @@ -25,7 +24,6 @@ import ( v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" @@ -222,44 +220,3 @@ func makeDeploymentResource(name types.NamespacedName, id string) *radappiov1alp }, } } - -func generateProviderConfig(radiusScope, azureScope, awsScope string) (string, error) { - if radiusScope == "" { - return "", fmt.Errorf("radiusScope is required") - } - - providerConfig := sdkclients.ProviderConfig{} - if awsScope != "" { - providerConfig.AWS = &sdkclients.AWS{ - Type: "aws", - Value: sdkclients.Value{ - Scope: awsScope, - }, - } - } - if azureScope != "" { - providerConfig.Az = &sdkclients.Az{ - Type: "azure", - Value: sdkclients.Value{ - Scope: azureScope, - }, - } - } - - providerConfig.Radius = &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: radiusScope, - }, - } - providerConfig.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: radiusScope, - }, - } - - b, err := json.Marshal(providerConfig) - - return string(b), err -} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-update-1.json b/pkg/controller/reconciler/testdata/deploymenttemplate-update-1.json new file mode 100644 index 0000000000..b2330d3d44 --- /dev/null +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-update-1.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "17470211592317605856" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "deploymenttemplate-update-env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "deploymenttemplate-update-env", + "location": "global", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "default" + } + } + } + } + } +} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-update-2.json b/pkg/controller/reconciler/testdata/deploymenttemplate-update-2.json new file mode 100644 index 0000000000..0587001980 --- /dev/null +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-update-2.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], + "_generator": { + "name": "bicep", + "version": "0.31.92.45157", + "templateHash": "17470211592317605856" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "deploymenttemplate-update-env": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "deploymenttemplate-update-env", + "location": "global", + "properties": { + "compute": { + "kind": "kubernetes", + "resourceId": "self", + "namespace": "notdefault" + } + } + } + } + } +} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json index 222ede5618..4a0a325347 100644 --- a/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-withresources.json @@ -18,11 +18,11 @@ } }, "resources": { - "env": { + "deploymenttemplate-withresources-env": { "import": "Radius", "type": "Applications.Core/environments@2023-10-01-preview", "properties": { - "name": "env", + "name": "deploymenttemplate-withresources-env", "location": "global", "properties": { "compute": { diff --git a/pkg/sdk/clients/providerconfig.go b/pkg/sdk/clients/providerconfig.go index 3fffee5637..768321e7a4 100644 --- a/pkg/sdk/clients/providerconfig.go +++ b/pkg/sdk/clients/providerconfig.go @@ -90,5 +90,5 @@ func GenerateProviderConfig(resourceGroup, awsScope, azureScope string) Provider // constructRadiusDeploymentScope constructs the scope for Radius deployments. func constructRadiusDeploymentScope(group string) string { - return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) + return fmt.Sprintf("/planes/radius/local/resourcegroups/%s", group) } diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index 3ffa49e1df..fa8c9bc045 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -49,7 +49,7 @@ import ( controller_runtime "sigs.k8s.io/controller-runtime/pkg/client" ) -func Test_DeploymentTemplate_Basic(t *testing.T) { +func Test_DeploymentTemplate_Env(t *testing.T) { ctx := testcontext.New(t) opts := rp.NewRPTestOptions(t) @@ -268,6 +268,8 @@ func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig return deploymentTemplate } +// waitForDeploymentTemplateReady watches the creation of the DeploymentTemplate object +// and waits for it to be in the "Ready" state. func waitForDeploymentTemplateReady(t *testing.T, ctx context.Context, name types.NamespacedName, client controller_runtime.WithWatch, initialVersion string) (*radappiov1alpha3.DeploymentTemplate, error) { // Based on https://gist.github.com/PrasadG193/52faed6499d2ec739f9630b9d044ffdc lister := &cache.ListWatch{ From 7d3cb5cd5eef28fbdc3cd17a6c67cb68079024db Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Thu, 23 Jan 2025 19:52:56 -0800 Subject: [PATCH 54/65] PR Signed-off-by: willdavsmith --- .../deploymenttemplate.yaml | 2 +- .../deploymenttemplate_reconciler_test.go | 48 +++++++++---------- pkg/sdk/clients/providerconfig.go | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml index d093174678..c7c0662d74 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/testdata/deploymenttemplate/deploymenttemplate.yaml @@ -8,7 +8,7 @@ spec: providerConfig: |- { "radius": { - "type": "radius", + "type": "Radius", "value": { "scope": "/planes/radius/local/resourceGroups/default" } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 6b8bd2e86b..f07b17c750 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -245,7 +245,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) // Verify that the Radius deployment contains the expected properties. expectedProperties := map[string]any{ @@ -256,18 +256,18 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-basic", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-basic", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-basic", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-basic", }, }, }, } - resource, err := radius.Resources("/planes/radius/local/resourcegroups/deploymenttemplate-basic", "Microsoft.Resources/deployments").Get(ctx, name.Name) + resource, err := radius.Resources("/planes/radius/local/resourceGroups/deploymenttemplate-basic", "Microsoft.Resources/deployments").Get(ctx, name.Name) require.NoError(t, err) require.Equal(t, expectedProperties, resource.Properties) @@ -359,7 +359,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { template, err := json.MarshalIndent(templateMap, "", " ") require.NoError(t, err) - scope := "/planes/radius/local/resourcegroups/deploymenttemplate-withresources" + scope := "/planes/radius/local/resourceGroups/deploymenttemplate-withresources" providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "") require.NoError(t, err) marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") @@ -376,21 +376,21 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { require.True(t, ok, "failed to find resource") resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, } state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) // DeploymentTemplate will be waiting for environment to be created. createEnvironment(radius, "deploymenttemplate-withresources", "deploymenttemplate-withresources-env") dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-withresources-env"} dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) // Verify that the Radius deployment contains the expected properties. resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) @@ -403,18 +403,18 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-withresources", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-withresources", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-withresources", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-withresources", }, }, }, "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, }, } require.Equal(t, expectedProperties, resource.Properties) @@ -469,7 +469,7 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { template, err := json.MarshalIndent(templateMap, "", " ") require.NoError(t, err) - scope := "/planes/radius/local/resourcegroups/deploymenttemplate-update" + scope := "/planes/radius/local/resourceGroups/deploymenttemplate-update" providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "") require.NoError(t, err) marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") @@ -486,21 +486,21 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { require.True(t, ok, "failed to find resource") resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, } state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) // DeploymentTemplate will be waiting for environment to be created. createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) // Verify that the Radius deployment contains the expected properties. resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) @@ -513,18 +513,18 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", }, }, }, "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, }, } require.Equal(t, expectedProperties, resource.Properties) @@ -569,21 +569,21 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { require.True(t, ok, "failed to find resource") resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, } state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) // DeploymentTemplate will be waiting for environment to be created. createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") dependencyName = types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} dependencyStatus = waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) // Verify that the Radius deployment contains the expected properties. resource, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) @@ -596,18 +596,18 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { Radius: &sdkclients.Radius{ Type: "Radius", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", }, }, Deployments: &sdkclients.Deployments{ Type: "Microsoft.Resources", Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourcegroups/deploymenttemplate-update", + Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", }, }, }, "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourcegroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, + map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, }, } require.Equal(t, expectedProperties, resource.Properties) diff --git a/pkg/sdk/clients/providerconfig.go b/pkg/sdk/clients/providerconfig.go index 768321e7a4..3fffee5637 100644 --- a/pkg/sdk/clients/providerconfig.go +++ b/pkg/sdk/clients/providerconfig.go @@ -90,5 +90,5 @@ func GenerateProviderConfig(resourceGroup, awsScope, azureScope string) Provider // constructRadiusDeploymentScope constructs the scope for Radius deployments. func constructRadiusDeploymentScope(group string) string { - return fmt.Sprintf("/planes/radius/local/resourcegroups/%s", group) + return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) } From d756b68a80fd275b2b51203551d1539fff9a1e61 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Fri, 24 Jan 2025 10:17:07 -0800 Subject: [PATCH 55/65] DEBUG Signed-off-by: willdavsmith --- .../generatekubernetesmanifest.go | 6 ++-- .../deploymenttemplate_reconciler_test.go | 32 +++++++----------- pkg/sdk/clients/providerconfig.go | 16 ++++++++- .../controller/resourcegroups/util.go | 1 + .../noncloud/deploymenttemplate_test.go | 33 ++++--------------- 5 files changed, 36 insertions(+), 52 deletions(-) diff --git a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go index 9cf716106c..73a5a2ca41 100644 --- a/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go +++ b/pkg/cli/cmd/bicep/generatekubernetesmanifest/generatekubernetesmanifest.go @@ -197,9 +197,7 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string return nil, err } - providerConfig := sdkclients.GenerateProviderConfig(r.Group, r.AWSScope, r.AzureScope) - - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + providerConfig, err := sdkclients.GenerateProviderConfig(r.Group, r.AWSScope, r.AzureScope).String() if err != nil { return nil, err } @@ -218,7 +216,7 @@ func (r *Runner) generateDeploymentTemplate(fileName string, template map[string "spec": map[string]any{ "template": string(marshalledTemplate), "parameters": params, - "providerConfig": string(marshalledProviderConfig), + "providerConfig": providerConfig, }, } diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index f07b17c750..8a1560c305 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -229,12 +229,10 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-basic", "", "") - require.NoError(t, err) - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-basic", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", string(marshalledProviderConfig), map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -276,7 +274,7 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: "{}", Parameters: map[string]string{}, - ProviderConfig: string(marshalledProviderConfig), + ProviderConfig: providerConfig, }, } @@ -306,12 +304,10 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) - providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-failurerecovery", "", "") - require.NoError(t, err) - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-failurerecovery", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", string(marshalledProviderConfig), map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -360,12 +356,10 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { require.NoError(t, err) scope := "/planes/radius/local/resourceGroups/deploymenttemplate-withresources" - providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "") - require.NoError(t, err) - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, string(template), string(marshalledProviderConfig), map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -424,7 +418,7 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: string(template), Parameters: map[string]string{}, - ProviderConfig: string(marshalledProviderConfig), + ProviderConfig: providerConfig, }, } @@ -470,12 +464,10 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { require.NoError(t, err) scope := "/planes/radius/local/resourceGroups/deploymenttemplate-update" - providerConfig := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "") - require.NoError(t, err) - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") + providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, string(template), string(marshalledProviderConfig), map[string]string{}) + deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) err = client.Create(ctx, deploymentTemplate) require.NoError(t, err) @@ -534,7 +526,7 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: string(template), Parameters: map[string]string{}, - ProviderConfig: string(marshalledProviderConfig), + ProviderConfig: providerConfig, }, } @@ -617,7 +609,7 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: string(template), Parameters: map[string]string{}, - ProviderConfig: string(marshalledProviderConfig), + ProviderConfig: providerConfig, }, } diff --git a/pkg/sdk/clients/providerconfig.go b/pkg/sdk/clients/providerconfig.go index 3fffee5637..aba2eb1e43 100644 --- a/pkg/sdk/clients/providerconfig.go +++ b/pkg/sdk/clients/providerconfig.go @@ -16,7 +16,10 @@ limitations under the License. package clients -import "fmt" +import ( + "encoding/json" + "fmt" +) const ( // ProviderTypeAzure is used to specify the provider configuration for Azure resources. @@ -92,3 +95,14 @@ func GenerateProviderConfig(resourceGroup, awsScope, azureScope string) Provider func constructRadiusDeploymentScope(group string) string { return fmt.Sprintf("/planes/radius/local/resourceGroups/%s", group) } + +// String returns the JSON representation of the ProviderConfig object +// in a string format. +func (p ProviderConfig) String() (string, error) { + b, err := json.MarshalIndent(p, "", " ") + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/pkg/ucp/frontend/controller/resourcegroups/util.go b/pkg/ucp/frontend/controller/resourcegroups/util.go index 5d41849821..bf4a975f2a 100644 --- a/pkg/ucp/frontend/controller/resourcegroups/util.go +++ b/pkg/ucp/frontend/controller/resourcegroups/util.go @@ -174,6 +174,7 @@ func ValidateResourceType(ctx context.Context, client database.Client, id resour } _, ok := locationResourceType.APIVersions[apiVersion] + fmt.Println("DEBUG - locationResourceType.APIVersions[apiVersion]: ", locationResourceType.APIVersions[apiVersion]) if !ok { return nil, &InvalidError{Message: fmt.Sprintf("api version %q is not supported for resource type %q by location %q", apiVersion, id.Type(), locationName)} } diff --git a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go index fa8c9bc045..9e90d92990 100644 --- a/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go +++ b/test/functional-portable/kubernetes/noncloud/deploymenttemplate_test.go @@ -18,7 +18,6 @@ package kubernetes_test import ( "context" - "encoding/json" "fmt" "os" "path" @@ -61,7 +60,7 @@ func Test_DeploymentTemplate_Env(t *testing.T) { fmt.Sprintf("namespace=%s", namespace), } - providerConfig, err := generateDefaultProviderConfig() + providerConfig, err := sdkclients.NewDefaultProviderConfig(name).String() require.NoError(t, err) parametersMap := createParametersMap(parameters) @@ -127,7 +126,7 @@ func Test_DeploymentTemplate_Module(t *testing.T) { fmt.Sprintf("namespace=%s", namespace), } - providerConfig, err := generateDefaultProviderConfig() + providerConfig, err := sdkclients.NewDefaultProviderConfig(name).String() require.NoError(t, err) parametersMap := createParametersMap(parameters) @@ -196,7 +195,7 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { fmt.Sprintf("namespace=%s", namespace), } - providerConfig, err := generateDefaultProviderConfig() + providerConfig, err := sdkclients.NewDefaultProviderConfig(name).String() require.NoError(t, err) parametersMap := createParametersMap(parameters) @@ -252,6 +251,7 @@ func Test_DeploymentTemplate_Recipe(t *testing.T) { }) } +// makeDeploymentTemplate returns a DeploymentTemplate object with the given name, template, providerConfig, and parameters. func makeDeploymentTemplate(name types.NamespacedName, template, providerConfig string, parameters map[string]string) *radappiov1alpha3.DeploymentTemplate { deploymentTemplate := &radappiov1alpha3.DeploymentTemplate{ ObjectMeta: metav1.ObjectMeta{ @@ -309,29 +309,8 @@ func waitForDeploymentTemplateReady(t *testing.T, ctx context.Context, name type } } -func generateDefaultProviderConfig() (string, error) { - providerConfig := sdkclients.ProviderConfig{} - - providerConfig.Radius = &sdkclients.Radius{ - Type: "radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - } - providerConfig.Deployments = &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/default", - }, - } - - marshalledProviderConfig, err := json.MarshalIndent(providerConfig, "", " ") - if err != nil { - return "", err - } - return string(marshalledProviderConfig), nil -} - +// createParametersMap creates a map of parameters from a list of parameters +// in the form of key=value. func createParametersMap(parameters []string) map[string]string { parametersMap := make(map[string]string) for _, param := range parameters { From 8e178720dae7097ab4d7a669308872f4c04a13a6 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sat, 25 Jan 2025 07:28:26 -0800 Subject: [PATCH 56/65] Test pass Signed-off-by: willdavsmith --- pkg/cli/deployment/deploy.go | 2 +- pkg/cli/deployment/deploy_test.go | 2 - .../v1alpha3/deploymentresource_types.go | 14 +- .../v1alpha3/deploymenttemplate_types.go | 14 +- .../reconciler/deployment_client.go | 82 +++ .../deploymentresource_reconciler.go | 47 +- .../deploymenttemplate_reconciler.go | 113 ++-- .../deploymenttemplate_reconciler_test.go | 519 ++++++++---------- .../mock_deployments_client_test.go | 200 +++++++ ...ent_test.go => mock_radius_client_test.go} | 29 +- pkg/controller/reconciler/poller.go | 34 ++ .../{client.go => radius_client.go} | 31 +- pkg/controller/service.go | 22 +- pkg/recipes/controllerconfig/config.go | 2 +- pkg/recipes/driver/bicep.go | 4 +- pkg/sdk/clients/resourcedeploymentsclient.go | 77 ++- .../controller/resourcegroups/util.go | 1 - 17 files changed, 727 insertions(+), 466 deletions(-) create mode 100644 pkg/controller/reconciler/deployment_client.go create mode 100644 pkg/controller/reconciler/mock_deployments_client_test.go rename pkg/controller/reconciler/{mock_client_test.go => mock_radius_client_test.go} (89%) create mode 100644 pkg/controller/reconciler/poller.go rename pkg/controller/reconciler/{client.go => radius_client.go} (93%) diff --git a/pkg/cli/deployment/deploy.go b/pkg/cli/deployment/deploy.go index b00bd0557d..d0aa7c3380 100644 --- a/pkg/cli/deployment/deploy.go +++ b/pkg/cli/deployment/deploy.go @@ -49,7 +49,7 @@ const ( type ResourceDeploymentClient struct { RadiusResourceGroup string - Client *sdkclients.ResourceDeploymentsClient + Client sdkclients.ResourceDeploymentsClient OperationsClient *sdkclients.ResourceDeploymentOperationsClient Tags map[string]*string } diff --git a/pkg/cli/deployment/deploy_test.go b/pkg/cli/deployment/deploy_test.go index 6b13080986..aaab3acf3f 100644 --- a/pkg/cli/deployment/deploy_test.go +++ b/pkg/cli/deployment/deploy_test.go @@ -54,8 +54,6 @@ func Test_GetProviderConfigs(t *testing.T) { func Test_GetProviderConfigsWithAzProvider(t *testing.T) { resourceDeploymentClient := ResourceDeploymentClient{ RadiusResourceGroup: "testrg", - Client: &sdkclients.ResourceDeploymentsClient{}, - OperationsClient: &sdkclients.ResourceDeploymentOperationsClient{}, } options := clients.DeploymentOptions{ diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go index e2e7fbc60c..ddc8cff33c 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymentresource_types.go @@ -20,18 +20,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// DeploymentResourceSpec defines the desired state of DeploymentResource +// DeploymentResourceSpec defines the desired state of a DeploymentResource resource. type DeploymentResourceSpec struct { - // Id is the resource Id. + // Id is the resource id of the Radius resource. Id string `json:"id,omitempty"` - - // ProviderConfig specifies the scope for resources - ProviderConfig string `json:"providerConfig,omitempty"` } -// DeploymentResourceStatus defines the observed state of DeploymentResource +// DeploymentResourceStatus defines the observed state of a DeploymentResource resource. type DeploymentResourceStatus struct { - // Id is the resource Id. + // Id is the resource id of the Radius resource. Id string `json:"id,omitempty"` // ObservedGeneration is the most recent generation observed for this DeploymentResource. @@ -42,9 +39,6 @@ type DeploymentResourceStatus struct { // Phrase indicates the current status of the Deployment Resource. Phrase DeploymentResourcePhrase `json:"phrase,omitempty"` - - // Message is a human-readable description of the status of the Deployment Resource. - Message string `json:"message,omitempty"` } // DeploymentResourcePhrase is a string representation of the current status of a Deployment Resource. diff --git a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go index 8472510aa0..8e97c9d186 100644 --- a/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go +++ b/pkg/controller/api/radapp.io/v1alpha3/deploymenttemplate_types.go @@ -20,7 +20,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// DeploymentTemplateSpec defines the desired state of DeploymentTemplate +// DeploymentTemplateSpec defines the desired state of a DeploymentTemplate resource. type DeploymentTemplateSpec struct { // Template is the ARM JSON manifest that defines the resources to deploy. Template string `json:"template,omitempty"` @@ -28,21 +28,18 @@ type DeploymentTemplateSpec struct { // Parameters is the ARM JSON parameters for the template. Parameters map[string]string `json:"parameters,omitempty"` - // ProviderConfig specifies the scope for resources + // ProviderConfig specifies the scopes for resources. ProviderConfig string `json:"providerConfig,omitempty"` } -// DeploymentTemplateStatus defines the observed state of DeploymentTemplate +// DeploymentTemplateStatus defines the observed state of a DeploymentTemplate resource. type DeploymentTemplateStatus struct { // ObservedGeneration is the most recent generation observed for this DeploymentTemplate. ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` - // StatusHash is a hash of the DeploymentTemplate's status. + // StatusHash is a hash of the DeploymentTemplate's state (template, parameters, and provider config). StatusHash string `json:"statusHash,omitempty"` - // Resource is the resource id of the deployment. - Resource string `json:"resource,omitempty"` - // OutputResources is a list of the resourceIDs that were created by the template on the last deployment. OutputResources []string `json:"outputResources,omitempty"` @@ -51,9 +48,6 @@ type DeploymentTemplateStatus struct { // Phrase indicates the current status of the Deployment Template. Phrase DeploymentTemplatePhrase `json:"phrase,omitempty"` - - // Message is a human-readable description of the status of the Deployment Template. - Message string `json:"message,omitempty"` } // DeploymentTemplatePhrase is a string representation of the current status of a Deployment Template. diff --git a/pkg/controller/reconciler/deployment_client.go b/pkg/controller/reconciler/deployment_client.go new file mode 100644 index 0000000000..4e3424933a --- /dev/null +++ b/pkg/controller/reconciler/deployment_client.go @@ -0,0 +1,82 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "github.com/radius-project/radius/pkg/sdk" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" +) + +type DeploymentClient interface { + ResourceDeployments() ResourceDeploymentsClient +} + +type ResourceDeploymentsClient interface { + CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) + ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) + Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) + ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) +} + +type DeploymentClientImpl struct { + connection sdk.Connection +} + +func NewDeploymentClient(connection sdk.Connection) *DeploymentClientImpl { + return &DeploymentClientImpl{connection: connection} +} + +var _ DeploymentClient = (*DeploymentClientImpl)(nil) + +func (rdc *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { + return rdc.inner.CreateOrUpdate(ctx, parameters, resourceID, apiVersion) +} + +func (rdc *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { + return rdc.inner.ContinueCreateOperation(ctx, resumeToken) +} + +func (rdc *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) { + return rdc.inner.Delete(ctx, resourceID, apiVersion) +} + +func (rdc *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) { + return rdc.inner.ContinueDeleteOperation(ctx, resumeToken) +} + +var _ ResourceDeploymentsClient = (*ResourceDeploymentsClientImpl)(nil) + +type ResourceDeploymentsClientImpl struct { + inner sdkclients.ResourceDeploymentsClient +} + +func (c *DeploymentClientImpl) ResourceDeployments() ResourceDeploymentsClient { + rdc, err := sdkclients.NewResourceDeploymentsClient(&sdkclients.Options{ + Cred: &aztoken.AnonymousCredential{}, + BaseURI: c.connection.Endpoint(), + ARMClientOptions: sdk.NewClientOptions(c.connection), + }) + if err != nil { + panic(fmt.Errorf("failed to create client: %w", err)) + } + + return &ResourceDeploymentsClientImpl{inner: rdc} +} diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 1e69c049a7..7a481c36ea 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -18,7 +18,6 @@ package reconciler import ( "context" - "encoding/json" "fmt" "strings" "time" @@ -33,7 +32,6 @@ import ( "github.com/go-logr/logr" "github.com/radius-project/radius/pkg/cli/clients" - "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/resources" @@ -55,6 +53,9 @@ type DeploymentResourceReconciler struct { // Radius is the Radius client. Radius RadiusClient + // DeploymentClient is the UCP Deployments client. + DeploymentClient DeploymentClient + // DelayInterval is the amount of time to wait between operations. DelayInterval time.Duration } @@ -100,17 +101,18 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R // NOTE: if reconcileOperation completes successfully, then it will return a "zero" result, // this means the operation has completed and we should continue processing. logger.Info("Operation completed successfully.") + // TODO (willsmith) return here? } else { logger.Info("Requeueing to continue operation.") return result, nil } } - if deploymentResource.ObjectMeta.DeletionTimestamp != nil { + if deploymentResource.DeletionTimestamp != nil { return r.reconcileDelete(ctx, &deploymentResource) } - logger.Info("Resource is in desired state.", "resourceId", deploymentResource.Spec.Id) + logger.Info("Resource is in desired state.") deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseReady deploymentResource.Status.Id = deploymentResource.Spec.Id @@ -128,13 +130,8 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - providerConfig := sdkclients.ProviderConfig{} - err := json.Unmarshal([]byte(deploymentResource.Spec.ProviderConfig), &providerConfig) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal providerConfig: %w", err) - } - poller, err := r.Radius.Resources(providerConfig.Deployments.Value.Scope, deploymentResourceType).ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + poller, err := r.DeploymentClient.ResourceDeployments().ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -223,13 +220,18 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) - logger.Info("Resource is being deleted.", "resourceId", deploymentResource.Spec.Id) + logger.Info("Resource is being deleted.") // Since we're going to reconcile, update the observed generation. // // We don't want to do this if we're in the middle of an operation, because we haven't // fully processed any status changes until the async operation completes. deploymentResource.Status.ObservedGeneration = deploymentResource.Generation + deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting + err := r.Client.Status().Update(ctx, deploymentResource) + if err != nil { + return ctrl.Result{}, err + } // Check if the resource is being used by another resource deploymentResourceList, err := listResourcesWithSameOwner(ctx, r.Client, deploymentResource.Namespace, deploymentResource.OwnerReferences[0]) @@ -248,20 +250,19 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } - poller, err := r.startDeleteOperation(ctx, deploymentResource) + deletePoller, err := r.startDeleteOperation(ctx, deploymentResource) if err != nil { logger.Error(err, "Unable to delete resource.") r.EventRecorder.Event(deploymentResource, corev1.EventTypeWarning, "ResourceError", err.Error()) return ctrl.Result{}, err - } else if poller != nil { + } else if deletePoller != nil && !deletePoller.Done() { // We've successfully started an operation. Update the status and requeue. - token, err := poller.ResumeToken() + token, err := deletePoller.ResumeToken() if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get operation token: %w", err) } deploymentResource.Status.Operation = &radappiov1alpha3.ResourceOperation{ResumeToken: token, OperationKind: radappiov1alpha3.OperationKindDelete} - deploymentResource.Status.Phrase = radappiov1alpha3.DeploymentResourcePhraseDeleting err = r.Client.Status().Update(ctx, deploymentResource) if err != nil { return ctrl.Result{}, err @@ -270,6 +271,7 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } + // If we get here then it means we can process the result of the operation. logger.Info("Resource is deleted.") // At this point we've cleaned up everything. We can remove the finalizer which will allow deletion of the @@ -283,26 +285,23 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl } } - logger.Info("Finalizer was not removed, requeueing.") - - err = r.Client.Status().Update(ctx, deploymentResource) - if err != nil { - return ctrl.Result{}, err - } - // If we get here, then we're in a bad state. We should have removed the finalizer, but we didn't. // We should requeue and try again. + logger.Info("Finalizer was not removed, requeueing.") + return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } -func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[sdkclients.ClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) resourceId := deploymentResource.Spec.Id + // TODO (willsmith) HARDCODED API VERSION + apiVersion := "2023-10-01-preview" logger.Info("Starting DELETE operation.") - poller, err := deleteResource(ctx, r.Radius, resourceId) + poller, err := r.DeploymentClient.ResourceDeployments().Delete(ctx, resourceId, apiVersion) if err != nil { return nil, err } else if poller != nil { diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index ed32ff86ca..35151228ce 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -26,31 +26,28 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/go-logr/logr" - "github.com/radius-project/radius/pkg/cli/clients_new/generated" + "github.com/google/uuid" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" corev1 "k8s.io/api/core/v1" ) -const ( - deploymentResourceType = "Microsoft.Resources/deployments" -) - // DeploymentTemplateReconciler reconciles a DeploymentTemplate object. type DeploymentTemplateReconciler struct { // Client is the Kubernetes client. Client client.Client // Scheme is the Kubernetes scheme. - Scheme *runtime.Scheme + Scheme *k8sruntime.Scheme // EventRecorder is the Kubernetes event recorder. EventRecorder record.EventRecorder @@ -58,6 +55,9 @@ type DeploymentTemplateReconciler struct { // Radius is the Radius client. Radius RadiusClient + // DeploymentClient is the UCP Deployments client. + DeploymentClient DeploymentClient + // DelayInterval is the amount of time to wait between operations. DelayInterval time.Duration } @@ -88,15 +88,16 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R // 2. Depending on the diff, create or delete `DeploymentResource` resources on the cluster. In the case of create, add the `DeploymentTemplate` as the owner of the `DeploymentResource` and set the `radapp.io/deployment-resource-finalizer` finalizer on the `DeploymentResource`. // 3. Update the `status.phrase` for the `DeploymentTemplate` to `Ready`. // 4. Continue processing. - // 3. If the operation failed, then update the `status.phrase` and `status.message` as `Failed` with the reason for the failure and continue processing. + // 3. If the operation failed, then update the `status.phrase` as `Failed` and continue processing. // 2. If the `DeploymentTemplate` is being deleted, then process deletion: - // 1. Remove the `radapp.io/deployment-template-finalizer` finalizer from the `DeploymentTemplate`. // 1. Since the `DeploymentResources` are owned by the `DeploymentTemplate`, the `DeploymentResource` resources will be deleted first. Once they are deleted, the `DeploymentTemplate` resource will be deleted. - // 4. If the `DeploymentTemplate` is not being deleted then process this as a create or update: + // 2. Once the dependent resources are deleted, remove the `radapp.io/deployment-template-finalizer` finalizer from the `DeploymentTemplate`. + // 3. If the `DeploymentTemplate` is not being deleted then process this as a create or update: // 1. Add the `radapp.io/deployment-template-finalizer` finalizer onto the `DeploymentTemplate` resource. - // 2. Queue a PUT operation against the Radius API to deploy the ARM JSON in the `spec.template` field with the parameters in the `spec.parameters` field. - // 3. Set the `status.phrase` for the `DeploymentTemplate` to `Updating` and the `status.operation` to the operation returned by the Radius API. - // 4. Continue processing. + // 2. Check if the desired state of the `DeploymentTemplate` resource matches the observed state. If it does, then the resource is up-to-date and we can continue processing. + // 3. Otherwise, queue a PUT operation against the Radius API to deploy the ARM JSON in the `spec.template` field with the parameters in the `spec.parameters` field. + // 4. Set the `status.phrase` for the `DeploymentTemplate` to `Updating` and the `status.operation` to the operation returned by the Radius API. + // 5. Continue processing. // // We do it this way because it guarantees that we only have one operation going at a time. @@ -106,7 +107,7 @@ func (r *DeploymentTemplateReconciler) Reconcile(ctx context.Context, req ctrl.R logger.Error(err, "Unable to reconcile in-progress operation.") return ctrl.Result{}, err } else if result.IsZero() { - // NOTE: if reconcileOperation completes successfully, then it will return a "zero" result, + // If reconcileOperation completes successfully, then it will return a "zero" result, // this means the operation has completed and we should continue processing. logger.Info("Operation completed successfully.") } else { @@ -127,12 +128,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - scope, err := ParseDeploymentScopeFromProviderConfig(deploymentTemplate.Spec.ProviderConfig) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to parse deployment scope: %w", err) - } - - poller, err := r.Radius.Resources(scope, deploymentResourceType).ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.DeploymentClient.ResourceDeployments().ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -155,7 +151,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - deploymentTemplate.Status.Message = err.Error() err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -168,11 +163,11 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d // Get outputResources from the response outputResources := make([]string, 0) - if resp.Properties["outputResources"] != nil { - outputResourceList := resp.Properties["outputResources"].([]any) - for _, resource := range outputResourceList { - outputResource := resource.(map[string]any) - outputResources = append(outputResources, outputResource["id"].(string)) + if resp.Properties != nil && resp.Properties.OutputResources != nil { + for _, resource := range resp.Properties.OutputResources { + if resource.ID != nil { + outputResources = append(outputResources, *resource.ID) + } } // Compare outputResources with existing DeploymentResources @@ -194,6 +189,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d if _, ok := existingOutputResources[outputResourceId]; !ok { // Resource is not present in deploymentTemplate.Status.OutputResources but is in outputResources, create it + logger.Info("Creating DeploymentResource.", "resourceId", outputResourceId) resourceName, err := generateDeploymentResourceName(outputResourceId) if err != nil { return ctrl.Result{}, err @@ -205,8 +201,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d Namespace: deploymentTemplate.Namespace, }, Spec: radappiov1alpha3.DeploymentResourceSpec{ - Id: outputResourceId, - ProviderConfig: deploymentTemplate.Spec.ProviderConfig, + Id: outputResourceId, }, } @@ -228,6 +223,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d for _, resource := range deploymentTemplate.Status.OutputResources { if _, ok := newOutputResources[resource]; !ok { // Resource is present in deploymentTemplate.Status.OutputResources but not in outputResources, delete it + logger.Info("Deleting resource.", "resourceId", resource) resourceName, err := generateDeploymentResourceName(resource) if err != nil { @@ -247,24 +243,19 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d } } - providerConfig := sdkclients.ProviderConfig{} - err = json.Unmarshal([]byte(deploymentTemplate.Spec.ProviderConfig), &providerConfig) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to unmarshal providerConfig: %w", err) - } - hash, err := computeHash(deploymentTemplate) if err != nil { return ctrl.Result{}, err } // If we get here, the operation was a success. Update the status and continue. - // - // NOTE: we don't need to save the status here, because we're going to continue reconciling. deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.OutputResources = outputResources deploymentTemplate.Status.StatusHash = hash - deploymentTemplate.Status.Resource = providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name + err = r.Client.Status().Update(ctx, deploymentTemplate) + if err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } @@ -276,7 +267,6 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d deploymentTemplate.Status.Operation = nil deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - deploymentTemplate.Status.Message = errorMessage.Error() err := r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -309,7 +299,6 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl logger.Error(err, "Unable to create or update resource.") r.EventRecorder.Event(deploymentTemplate, corev1.EventTypeWarning, "ResourceError", err.Error()) deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseFailed - deploymentTemplate.Status.Message = err.Error() err = r.Client.Status().Update(ctx, deploymentTemplate) if err != nil { return ctrl.Result{}, err @@ -334,7 +323,7 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl } // If we get here then it means we can process the result of the operation. - logger.Info("Resource is in desired state.", "resourceId", deploymentTemplate.Status.Resource) + logger.Info("Resource is in desired state.") deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseReady err = r.Client.Status().Update(ctx, deploymentTemplate) @@ -349,7 +338,7 @@ func (r *DeploymentTemplateReconciler) reconcileUpdate(ctx context.Context, depl func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (ctrl.Result, error) { logger := ucplog.FromContextOrDiscard(ctx) - logger.Info("Resource is being deleted.", "resourceId", deploymentTemplate.Status.Resource) + logger.Info("Resource is being deleted.") // Since we're going to reconcile, update the observed generation. // @@ -390,18 +379,13 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl } } - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } logger.Info("Resource is deleted.") // At this point we've cleaned up everything. We can remove the finalizer which will allow - // deletion of the DeploymentTemplate + // deletion of the DeploymentTemplate. if controllerutil.RemoveFinalizer(deploymentTemplate, DeploymentTemplateFinalizer) { deploymentTemplate.Status.ObservedGeneration = deploymentTemplate.Generation deploymentTemplate.Status.Phrase = radappiov1alpha3.DeploymentTemplatePhraseDeleted @@ -427,7 +411,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } -func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) @@ -466,16 +450,22 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con return nil, fmt.Errorf("failed to create resource group: %w", err) } - logger.Info("Starting PUT operation.") - properties := map[string]any{ - "mode": "Incremental", - "providerConfig": providerConfig, - "template": template, - "parameters": specParameters, - } + deploymentName := fmt.Sprintf("deploymenttemplate-%v", uuid.New().String()) + resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + "Microsoft.Resources/deployments" + "/" + deploymentName - resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + deploymentResourceType + "/" + deploymentTemplate.Name - poller, err := createOrUpdateResource(ctx, r.Radius, resourceID, properties) + logger.Info("Starting PUT operation.") + poller, err := r.DeploymentClient.ResourceDeployments().CreateOrUpdate(ctx, + sdkclients.Deployment{ + Properties: &sdkclients.DeploymentProperties{ + Template: template, + Parameters: specParameters, + ProviderConfig: providerConfig, + Mode: armresources.DeploymentModeIncremental, + }, + }, + resourceID, + sdkclients.DeploymentsClientAPIVersion, + ) if err != nil { return nil, err } else if poller != nil { @@ -483,12 +473,6 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con } // Update was synchronous - deploymentTemplate.Status.Resource = resourceID - err = r.Client.Status().Update(ctx, deploymentTemplate) - if err != nil { - return nil, err - } - return nil, nil } @@ -534,7 +518,8 @@ func isOwnedBy(resource radappiov1alpha3.DeploymentResource, owner *radappiov1al return false } -// computeHash computes a hash of the DeploymentTemplate's spec (desired state). +// computeHash computes a hash of the DeploymentTemplate's spec (desired state) +// to save in the status (observed state). func computeHash(deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (string, error) { b, err := json.Marshal(deploymentTemplate.Spec) if err != nil { diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 8a1560c305..7c9b6e2843 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/radius-project/radius/pkg/cli/clients_new/generated" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" @@ -35,7 +35,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" crconfig "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) @@ -46,7 +46,7 @@ const ( deploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 ) -func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client) { +func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, *mockDeploymentClient, k8sclient.Client) { SkipWithoutEnvironment(t) // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail @@ -73,25 +73,28 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client }) require.NoError(t, err) - radius := NewMockRadiusClient() + mockRadiusClient := NewMockRadiusClient() + mockDeploymentClient := NewMockDeploymentClient() // Set up DeploymentTemplateReconciler. err = (&DeploymentTemplateReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), - Radius: radius, - DelayInterval: deploymentTemplateTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: mockRadiusClient, + DeploymentClient: mockDeploymentClient, + DelayInterval: deploymentTemplateTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) // Set up DeploymentResourceReconciler. err = (&DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: radius, - DelayInterval: DeploymentResourceTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: mockRadiusClient, + DeploymentClient: mockDeploymentClient, + DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) @@ -100,7 +103,7 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, client.Client require.NoError(t, err) }() - return radius, mgr.GetClient() + return mockRadiusClient, mockDeploymentClient, mgr.GetClient() } func Test_DeploymentTemplateReconciler_ComputeHash(t *testing.T) { @@ -223,51 +226,27 @@ func Test_DeploymentTemplateReconciler_IsUpToDate(t *testing.T) { func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) - radius, client := SetupDeploymentTemplateTest(t) + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "deploymenttemplate-basic", Name: "test-deploymenttemplate-basic"} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-basic", Name: "test-deploymenttemplate-basic"} + err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) require.NoError(t, err) - providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-basic", "", "").String() + providerConfig, err := sdkclients.NewDefaultProviderConfig("deploymenttemplate-basic").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) - err = client.Create(ctx, deploymentTemplate) + deploymentTemplate := makeDeploymentTemplate(namespacedName, "{}", providerConfig, map[string]string{}) + err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) // Wait for the DeploymentTemplate to enter the updating state. - status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) - radius.CompleteOperation(status.Operation.ResumeToken, nil) + // Complete the operation. + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, nil) // DeploymentTemplate should be ready after the operation completes. - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-basic/providers/Microsoft.Resources/deployments/test-deploymenttemplate-basic", status.Resource) - - // Verify that the Radius deployment contains the expected properties. - expectedProperties := map[string]any{ - "mode": "Incremental", - "template": map[string]any{}, - "parameters": map[string]map[string]string{}, - "providerConfig": sdkclients.ProviderConfig{ - Radius: &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-basic", - }, - }, - Deployments: &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-basic", - }, - }, - }, - } - resource, err := radius.Resources("/planes/radius/local/resourceGroups/deploymenttemplate-basic", "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) - require.Equal(t, expectedProperties, resource.Properties) + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) // Verify that the DeploymentTemplate contains the expected properties. expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ @@ -284,11 +263,11 @@ func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { require.Equal(t, expectedStatusHash, status.StatusHash) // Delete the DeploymentTemplate - err = client.Delete(ctx, deploymentTemplate) + err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) // Wait for the DeploymentTemplate to be deleted. - waitForDeploymentTemplateStateDeleted(t, client, name) + waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) } func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { @@ -298,53 +277,54 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { // and verify that the controller will (eventually) retry these operations. ctx := testcontext.New(t) - radius, client := SetupDeploymentTemplateTest(t) + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "deploymenttemplate-failurerecovery", Name: "test-deploymenttemplate-failurerecovery"} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-failurerecovery", Name: "test-deploymenttemplate-failurerecovery"} + err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) require.NoError(t, err) providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-failurerecovery", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, "{}", providerConfig, map[string]string{}) - err = client.Create(ctx, deploymentTemplate) + deploymentTemplate := makeDeploymentTemplate(namespacedName, "{}", providerConfig, map[string]string{}) + err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) // Wait for the DeploymentTemplate to enter the updating state. - status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation, but make it fail. operation := status.Operation - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { state.err = errors.New("failure") - resource, ok := radius.resources[state.resourceID] + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] require.True(t, ok, "failed to find resource") - resource.Properties["provisioningState"] = "Failed" - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + resource.Properties.ProvisioningState = to.Ptr(armresources.ProvisioningStateFailed) + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should (eventually) start a new provisioning operation - status = waitForDeploymentTemplateStateUpdating(t, client, name, operation) + status = waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, operation) // Complete the operation, successfully this time. - radius.CompleteOperation(status.Operation.ResumeToken, nil) - _ = waitForDeploymentTemplateStateReady(t, client, name) + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, nil) + _ = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - err = client.Delete(ctx, deploymentTemplate) + // Delete the DeploymentTemplate + err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) - waitForDeploymentTemplateStateDeleted(t, client, name) + waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) } func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { ctx := testcontext.New(t) - radius, client := SetupDeploymentTemplateTest(t) + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - name := types.NamespacedName{Namespace: "deploymenttemplate-withresources", Name: "test-deploymenttemplate-withresources"} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-withresources", Name: "test-deploymenttemplate-withresources"} + err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) require.NoError(t, err) fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-withresources.json")) @@ -355,65 +335,32 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { template, err := json.MarshalIndent(templateMap, "", " ") require.NoError(t, err) - scope := "/planes/radius/local/resourceGroups/deploymenttemplate-withresources" providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "").String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) - err = client.Create(ctx, deploymentTemplate) + deploymentTemplate := makeDeploymentTemplate(namespacedName, string(template), providerConfig, map[string]string{}) + err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) - status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) + status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := radius.resources[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] require.True(t, ok, "failed to find resource") - resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, + resource.Properties.OutputResources = []*armresources.ResourceReference{ + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env")}, } - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should be ready after the operation completes. - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Microsoft.Resources/deployments/test-deploymenttemplate-withresources", status.Resource) - - // DeploymentTemplate will be waiting for environment to be created. - createEnvironment(radius, "deploymenttemplate-withresources", "deploymenttemplate-withresources-env") + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-withresources-env"} - dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) + dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-withresources-env"} + dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) - // Verify that the Radius deployment contains the expected properties. - resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) - expectedProperties := map[string]any{ - "mode": "Incremental", - "template": templateMap, - "parameters": map[string]map[string]string{}, - "providerConfig": sdkclients.ProviderConfig{ - Radius: &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-withresources", - }, - }, - Deployments: &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-withresources", - }, - }, - }, - "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env"}, - }, - } - require.Equal(t, expectedProperties, resource.Properties) - - // Verify that the DeploymentTemplate contains the expected properties. expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ Spec: radappiov1alpha3.DeploymentTemplateSpec{ Template: string(template), @@ -421,204 +368,174 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { ProviderConfig: providerConfig, }, } - expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) require.NoError(t, err) require.Equal(t, expectedStatusHash, status.StatusHash) - err = client.Delete(ctx, deploymentTemplate) + // Trigger deletion of the DeploymentTemplate. + err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) - waitForDeploymentTemplateStateDeleting(t, client, name) - - dependencyStatus = waitForDeploymentResourceStateDeleting(t, client, dependencyName, nil) + // The DeploymentTemplate should be in the deleting state. + waitForDeploymentTemplateStateDeleting(t, k8sClient, namespacedName) - // Delete the environment. - deleteEnvironment(radius, "deploymenttemplate-withresources", "deploymenttemplate-withresources-env") + // Get the status of the dependency (DeploymentResource resource). + dependencyStatus = waitForDeploymentResourceStateDeleting(t, k8sClient, dependencyName, nil) // Complete the delete operation on the DeploymentResource. - radius.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) - - waitForDeploymentResourceDeleted(t, client, dependencyName) - waitForDeploymentTemplateStateDeleted(t, client, name) -} - -func Test_DeploymentTemplateReconciler_Update(t *testing.T) { - // This test tests our ability to update a DeploymentTemplate. - // We create a DeploymentTemplate, update it, and verify that the Radius resource is updated accordingly. - - ctx := testcontext.New(t) - radius, client := SetupDeploymentTemplateTest(t) - - name := types.NamespacedName{Namespace: "deploymenttemplate-update", Name: "test-deploymenttemplate-update"} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) - require.NoError(t, err) + mockDeploymentClient.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) - fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-update-1.json")) - require.NoError(t, err) - templateMap := map[string]any{} - err = json.Unmarshal(fileContent, &templateMap) - require.NoError(t, err) - template, err := json.MarshalIndent(templateMap, "", " ") - require.NoError(t, err) - - scope := "/planes/radius/local/resourceGroups/deploymenttemplate-update" - providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "").String() - require.NoError(t, err) - - deploymentTemplate := makeDeploymentTemplate(name, string(template), providerConfig, map[string]string{}) - err = client.Create(ctx, deploymentTemplate) - require.NoError(t, err) - - status := waitForDeploymentTemplateStateUpdating(t, client, name, nil) - - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := radius.resources[state.resourceID] - require.True(t, ok, "failed to find resource") - - resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, - } - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} - }) - - // DeploymentTemplate should be ready after the operation completes. - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) - - // DeploymentTemplate will be waiting for environment to be created. - createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") - - dependencyName := types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} - dependencyStatus := waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) - - // Verify that the Radius deployment contains the expected properties. - resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) - expectedProperties := map[string]any{ - "mode": "Incremental", - "template": templateMap, - "parameters": map[string]map[string]string{}, - "providerConfig": sdkclients.ProviderConfig{ - Radius: &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", - }, - }, - Deployments: &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", - }, - }, - }, - "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, - }, - } - require.Equal(t, expectedProperties, resource.Properties) - - // Verify that the DeploymentTemplate contains the expected properties. - expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ - Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: string(template), - Parameters: map[string]string{}, - ProviderConfig: providerConfig, - }, - } - - expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) - require.NoError(t, err) - require.Equal(t, expectedStatusHash, status.StatusHash) - - // Re-deploy the DeploymentTemplate with a new template. - - fileContent, err = os.ReadFile(path.Join("testdata", "deploymenttemplate-update-2.json")) - require.NoError(t, err) - templateMap = map[string]any{} - err = json.Unmarshal(fileContent, &templateMap) - require.NoError(t, err) - template, err = json.MarshalIndent(templateMap, "", " ") - require.NoError(t, err) - - newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} - err = client.Get(ctx, name, &newDeploymentTemplate) - require.NoError(t, err) - - // Update the template - newDeploymentTemplate.Spec.Template = string(template) - - err = client.Update(ctx, &newDeploymentTemplate) - require.NoError(t, err) - - status = waitForDeploymentTemplateStateUpdating(t, client, name, nil) - - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := radius.resources[state.resourceID] - require.True(t, ok, "failed to find resource") - - resource.Properties["outputResources"] = []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, - } - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} - }) - - // DeploymentTemplate should be ready after the operation completes. - status = waitForDeploymentTemplateStateReady(t, client, name) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Microsoft.Resources/deployments/test-deploymenttemplate-update", status.Resource) - - // DeploymentTemplate will be waiting for environment to be created. - createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") - - dependencyName = types.NamespacedName{Namespace: name.Namespace, Name: "deploymenttemplate-update-env"} - dependencyStatus = waitForDeploymentResourceStateReady(t, client, dependencyName) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) - - // Verify that the Radius deployment contains the expected properties. - resource, err = radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, name.Name) - require.NoError(t, err) - expectedProperties = map[string]any{ - "mode": "Incremental", - "template": templateMap, - "parameters": map[string]map[string]string{}, - "providerConfig": sdkclients.ProviderConfig{ - Radius: &sdkclients.Radius{ - Type: "Radius", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", - }, - }, - Deployments: &sdkclients.Deployments{ - Type: "Microsoft.Resources", - Value: sdkclients.Value{ - Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", - }, - }, - }, - "outputResources": []any{ - map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, - }, - } - require.Equal(t, expectedProperties, resource.Properties) - - // Verify that the DeploymentTemplate contains the expected properties. - expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ - Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: string(template), - Parameters: map[string]string{}, - ProviderConfig: providerConfig, - }, - } - - expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) - require.NoError(t, err) - require.Equal(t, expectedStatusHash, status.StatusHash) + waitForDeploymentResourceDeleted(t, k8sClient, dependencyName) + waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) } -func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { +// func Test_DeploymentTemplateReconciler_Update(t *testing.T) { +// // This test tests our ability to update a DeploymentTemplate. +// // We create a DeploymentTemplate, update it, and verify that the Radius resource is updated accordingly. + +// ctx := testcontext.New(t) +// _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + +// namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-update", Name: "test-deploymenttemplate-update"} +// err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) +// require.NoError(t, err) + +// fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-update-1.json")) +// require.NoError(t, err) +// templateMap := map[string]any{} +// err = json.Unmarshal(fileContent, &templateMap) +// require.NoError(t, err) +// template, err := json.MarshalIndent(templateMap, "", " ") +// require.NoError(t, err) + +// scope := "/planes/radius/local/resourceGroups/deploymenttemplate-update" +// providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "").String() +// require.NoError(t, err) + +// deploymentTemplate := makeDeploymentTemplate(namespacedName, string(template), providerConfig, map[string]string{}) +// err = k8sClient.Create(ctx, deploymentTemplate) +// require.NoError(t, err) + +// status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + +// radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { +// resource, ok := radius.resources[state.resourceID] +// require.True(t, ok, "failed to find resource") + +// resource.Properties["outputResources"] = []any{ +// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, +// } +// state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} +// }) + +// // DeploymentTemplate should be ready after the operation completes. +// status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + +// // DeploymentTemplate will be waiting for environment to be created. +// createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") + +// dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} +// dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) +// require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + +// // Verify that the Radius deployment contains the expected properties. +// resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, namespacedName.Name) +// require.NoError(t, err) +// expectedProperties := map[string]any{ +// "mode": "Incremental", +// "template": templateMap, +// "parameters": map[string]map[string]string{}, +// "providerConfig": sdkclients.ProviderConfig{ +// Radius: &sdkclients.Radius{ +// Type: "Radius", +// Value: sdkclients.Value{ +// Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", +// }, +// }, +// Deployments: &sdkclients.Deployments{ +// Type: "Microsoft.Resources", +// Value: sdkclients.Value{ +// Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", +// }, +// }, +// }, +// "outputResources": []any{ +// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, +// }, +// } +// require.Equal(t, expectedProperties, resource.Properties) + +// // Verify that the DeploymentTemplate contains the expected properties. +// expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ +// Spec: radappiov1alpha3.DeploymentTemplateSpec{ +// Template: string(template), +// Parameters: map[string]string{}, +// ProviderConfig: providerConfig, +// }, +// } + +// expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) +// require.NoError(t, err) +// require.Equal(t, expectedStatusHash, status.StatusHash) + +// // Re-deploy the DeploymentTemplate with a new template. + +// fileContent, err = os.ReadFile(path.Join("testdata", "deploymenttemplate-update-2.json")) +// require.NoError(t, err) +// templateMap = map[string]any{} +// err = json.Unmarshal(fileContent, &templateMap) +// require.NoError(t, err) +// template, err = json.MarshalIndent(templateMap, "", " ") +// require.NoError(t, err) + +// newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} +// err = k8sClient.Get(ctx, namespacedName, &newDeploymentTemplate) +// require.NoError(t, err) + +// // Update the template +// newDeploymentTemplate.Spec.Template = string(template) + +// err = k8sClient.Update(ctx, &newDeploymentTemplate) +// require.NoError(t, err) + +// status = waitForDeploymentTemplateStateUpdating(t, k8sClient, name, nil) + +// mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { +// resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] +// require.True(t, ok, "failed to find resource") + +// resource.Properties["outputResources"] = []any{ +// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, +// } +// state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} +// }) + +// // DeploymentTemplate should be ready after the operation completes. +// status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + +// // DeploymentTemplate will be waiting for environment to be created. +// createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") + +// dependencyName = types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} +// dependencyStatus = waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) +// require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + +// // Verify that the DeploymentTemplate contains the expected properties. +// expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ +// Spec: radappiov1alpha3.DeploymentTemplateSpec{ +// Template: string(template), +// Parameters: map[string]string{}, +// ProviderConfig: providerConfig, +// }, +// } + +// expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) +// require.NoError(t, err) +// require.Equal(t, expectedStatusHash, status.StatusHash) +// } + +func waitForDeploymentTemplateStateUpdating(t *testing.T, client k8sclient.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) logger := t @@ -647,7 +564,7 @@ func waitForDeploymentTemplateStateUpdating(t *testing.T, client client.Client, return status } -func waitForDeploymentTemplateStateReady(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { +func waitForDeploymentTemplateStateReady(t *testing.T, client k8sclient.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) logger := t @@ -670,7 +587,7 @@ func waitForDeploymentTemplateStateReady(t *testing.T, client client.Client, nam return status } -func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { +func waitForDeploymentTemplateStateDeleting(t *testing.T, client k8sclient.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) logger := t @@ -691,7 +608,7 @@ func waitForDeploymentTemplateStateDeleting(t *testing.T, client client.Client, return status } -func waitForDeploymentTemplateStateDeleted(t *testing.T, client client.Client, name types.NamespacedName) { +func waitForDeploymentTemplateStateDeleted(t *testing.T, client k8sclient.Client, name types.NamespacedName) { ctx := testcontext.New(t) logger := t diff --git a/pkg/controller/reconciler/mock_deployments_client_test.go b/pkg/controller/reconciler/mock_deployments_client_test.go new file mode 100644 index 0000000000..0ce8f709e1 --- /dev/null +++ b/pkg/controller/reconciler/mock_deployments_client_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "net/http" + "sync" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/google/uuid" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" +) + +// This file contains mocks for the DeploymentClient interface. + +func NewMockDeploymentClient() *mockDeploymentClient { + return &mockDeploymentClient{ + resourceDeployments: map[string]sdkclients.ClientCreateOrUpdateResponse{}, + operations: map[string]*operationState{}, + + lock: &sync.Mutex{}, + } +} + +var _ DeploymentClient = (*mockDeploymentClient)(nil) + +type mockDeploymentClient struct { + resourceDeployments map[string]sdkclients.ClientCreateOrUpdateResponse + operations map[string]*operationState + + lock *sync.Mutex +} + +func (dc *mockDeploymentClient) ResourceDeployments() ResourceDeploymentsClient { + return &mockResourceDeploymentsClient{mock: dc} +} + +func (dc *mockDeploymentClient) CompleteOperation(operationID string, update func(state *operationState)) { + dc.lock.Lock() + defer dc.lock.Unlock() + + state, ok := dc.operations[operationID] + if !ok { + panic("operation not found: " + operationID) + } + + if update != nil { + update(state) + } + + state.complete = true +} + +var _ ResourceDeploymentsClient = (*mockResourceDeploymentsClient)(nil) + +type mockResourceDeploymentsClient struct { + mock *mockDeploymentClient +} + +func (rdc *mockResourceDeploymentsClient) CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { + rdc.mock.lock.Lock() + defer rdc.mock.lock.Unlock() + + value := sdkclients.ClientCreateOrUpdateResponse{ + DeploymentExtended: armresources.DeploymentExtended{ + ID: &resourceID, + Properties: &armresources.DeploymentPropertiesExtended{}, + }, + } + state := &operationState{ + kind: http.MethodPut, + resourceID: resourceID, + value: value, + } + + operationID := uuid.New().String() + rdc.mock.resourceDeployments[resourceID] = value + rdc.mock.operations[operationID] = state + + return &mockDeploymentClientPoller[sdkclients.ClientCreateOrUpdateResponse]{ + mock: rdc.mock, + operationID: operationID, + state: state, + }, nil +} + +func (rdc *mockResourceDeploymentsClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { + rdc.mock.lock.Lock() + defer rdc.mock.lock.Unlock() + + state, ok := rdc.mock.operations[resumeToken] + if !ok { + return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} + } + + return &mockDeploymentClientPoller[sdkclients.ClientCreateOrUpdateResponse]{ + operationID: resumeToken, + mock: rdc.mock, + state: state, + }, nil +} + +func (rdc *mockResourceDeploymentsClient) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) { + rdc.mock.lock.Lock() + defer rdc.mock.lock.Unlock() + + state := &operationState{ + kind: http.MethodDelete, + resourceID: resourceID, + value: sdkclients.ClientDeleteResponse{ + DeploymentExtended: armresources.DeploymentExtended{ + ID: &resourceID, + Properties: &armresources.DeploymentPropertiesExtended{}, + }, + }, + } + + operationID := uuid.New().String() + rdc.mock.operations[operationID] = state + + return &mockDeploymentClientPoller[sdkclients.ClientDeleteResponse]{ + mock: rdc.mock, + operationID: operationID, + state: state, + }, nil +} + +func (rdc *mockResourceDeploymentsClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) { + rdc.mock.lock.Lock() + defer rdc.mock.lock.Unlock() + + state, ok := rdc.mock.operations[resumeToken] + if !ok { + return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} + } + + return &mockDeploymentClientPoller[sdkclients.ClientDeleteResponse]{ + operationID: resumeToken, + mock: rdc.mock, + state: state, + }, nil +} + +var _ Poller[sdkclients.ClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[sdkclients.ClientCreateOrUpdateResponse])(nil) + +type mockDeploymentClientPoller[T any] struct { + operationID string + mock *mockDeploymentClient + state *operationState +} + +func (mp *mockDeploymentClientPoller[T]) Done() bool { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + return mp.state.complete // Status updates are delivered via the Poll function. +} + +func (mp *mockDeploymentClientPoller[T]) Poll(ctx context.Context) (*http.Response, error) { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + // NOTE: this is ok because our code ignores the actual result. + mp.state = mp.mock.operations[mp.operationID] + return nil, nil +} + +func (mp *mockDeploymentClientPoller[T]) Result(ctx context.Context) (T, error) { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + if mp.state.complete && mp.state.err != nil { + return mp.state.value.(T), mp.state.err + } else if mp.state.complete { + return mp.state.value.(T), nil + } + + panic("operation not done") +} + +func (mp *mockDeploymentClientPoller[T]) ResumeToken() (string, error) { + return mp.operationID, nil +} diff --git a/pkg/controller/reconciler/mock_client_test.go b/pkg/controller/reconciler/mock_radius_client_test.go similarity index 89% rename from pkg/controller/reconciler/mock_client_test.go rename to pkg/controller/reconciler/mock_radius_client_test.go index d5eb8b5008..76c60f769b 100644 --- a/pkg/controller/reconciler/mock_client_test.go +++ b/pkg/controller/reconciler/mock_radius_client_test.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/google/uuid" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/cli/clients_new/generated" @@ -196,7 +197,7 @@ func (cc *mockContainerClient) BeginCreateOrUpdate(ctx context.Context, containe cc.mock.containers[id] = resource cc.mock.operations[operationID] = state - return &mockPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: operationID, state: state}, nil + return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: operationID, state: state}, nil } func (cc *mockContainerClient) BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { @@ -211,7 +212,7 @@ func (cc *mockContainerClient) BeginDelete(ctx context.Context, containerName st operationID := uuid.New().String() cc.mock.operations[operationID] = state - return &mockPoller[corerpv20231001preview.ContainersClientDeleteResponse]{mock: cc.mock, operationID: operationID, state: state}, nil + return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientDeleteResponse]{mock: cc.mock, operationID: operationID, state: state}, nil } func (cc *mockContainerClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { @@ -223,7 +224,7 @@ func (cc *mockContainerClient) ContinueCreateOperation(ctx context.Context, resu panic("operation not found: " + resumeToken) } - return &mockPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: resumeToken, state: state}, nil + return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: resumeToken, state: state}, nil } func (cc *mockContainerClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { @@ -235,7 +236,7 @@ func (cc *mockContainerClient) ContinueDeleteOperation(ctx context.Context, resu panic("operation not found: " + resumeToken) } - return &mockPoller[corerpv20231001preview.ContainersClientDeleteResponse]{mock: cc.mock, operationID: resumeToken, state: state}, nil + return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientDeleteResponse]{mock: cc.mock, operationID: resumeToken, state: state}, nil } func (cc *mockContainerClient) Get(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientGetOptions) (corerpv20231001preview.ContainersClientGetResponse, error) { @@ -333,7 +334,7 @@ func (rc *mockResourceClient) BeginCreateOrUpdate(ctx context.Context, resourceN rc.mock.resources[id] = resource rc.mock.operations[operationID] = state - return &mockPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: operationID, state: state}, nil + return &mockRadiusClientPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: operationID, state: state}, nil } func (rc *mockResourceClient) BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (Poller[generated.GenericResourcesClientDeleteResponse], error) { @@ -348,7 +349,7 @@ func (rc *mockResourceClient) BeginDelete(ctx context.Context, resourceName stri operationID := uuid.New().String() rc.mock.operations[operationID] = state - return &mockPoller[generated.GenericResourcesClientDeleteResponse]{mock: rc.mock, operationID: operationID, state: state}, nil + return &mockRadiusClientPoller[generated.GenericResourcesClientDeleteResponse]{mock: rc.mock, operationID: operationID, state: state}, nil } func (rc *mockResourceClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { @@ -360,7 +361,7 @@ func (rc *mockResourceClient) ContinueCreateOperation(ctx context.Context, resum panic("operation not found: " + resumeToken) } - return &mockPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: resumeToken, state: state}, nil + return &mockRadiusClientPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: resumeToken, state: state}, nil } func (rc *mockResourceClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientDeleteResponse], error) { @@ -372,7 +373,7 @@ func (rc *mockResourceClient) ContinueDeleteOperation(ctx context.Context, resum panic("operation not found: " + resumeToken) } - return &mockPoller[generated.GenericResourcesClientDeleteResponse]{mock: rc.mock, operationID: resumeToken, state: state}, nil + return &mockRadiusClientPoller[generated.GenericResourcesClientDeleteResponse]{mock: rc.mock, operationID: resumeToken, state: state}, nil } func (rc *mockResourceClient) Get(ctx context.Context, resourceName string) (generated.GenericResourcesClientGetResponse, error) { @@ -417,22 +418,22 @@ func (rc *mockResourceClient) ListSecrets(ctx context.Context, resourceName stri return generated.GenericResourcesClientListSecretsResponse{Value: secrets}, nil } -var _ Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse] = (*mockPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse])(nil) +var _ Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse])(nil) -type mockPoller[T any] struct { +type mockRadiusClientPoller[T any] struct { operationID string mock *mockRadiusClient state *operationState } -func (mp *mockPoller[T]) Done() bool { +func (mp *mockRadiusClientPoller[T]) Done() bool { mp.mock.lock.Lock() defer mp.mock.lock.Unlock() return mp.state.complete // Status updates are delivered via the Poll function. } -func (mp *mockPoller[T]) Poll(ctx context.Context) (*http.Response, error) { +func (mp *mockRadiusClientPoller[T]) Poll(ctx context.Context) (*http.Response, error) { mp.mock.lock.Lock() defer mp.mock.lock.Unlock() @@ -441,7 +442,7 @@ func (mp *mockPoller[T]) Poll(ctx context.Context) (*http.Response, error) { return nil, nil } -func (mp *mockPoller[T]) Result(ctx context.Context) (T, error) { +func (mp *mockRadiusClientPoller[T]) Result(ctx context.Context) (T, error) { mp.mock.lock.Lock() defer mp.mock.lock.Unlock() @@ -454,6 +455,6 @@ func (mp *mockPoller[T]) Result(ctx context.Context) (T, error) { panic("operation not done") } -func (mp *mockPoller[T]) ResumeToken() (string, error) { +func (mp *mockRadiusClientPoller[T]) ResumeToken() (string, error) { return mp.operationID, nil } diff --git a/pkg/controller/reconciler/poller.go b/pkg/controller/reconciler/poller.go new file mode 100644 index 0000000000..d349200061 --- /dev/null +++ b/pkg/controller/reconciler/poller.go @@ -0,0 +1,34 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "net/http" + + azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" +) + +type Poller[T any] interface { + Done() bool + Poll(ctx context.Context) (*http.Response, error) + Result(ctx context.Context) (T, error) + ResumeToken() (string, error) +} + +var _ Poller[sdkclients.ClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[sdkclients.ClientCreateOrUpdateResponse])(nil) diff --git a/pkg/controller/reconciler/client.go b/pkg/controller/reconciler/radius_client.go similarity index 93% rename from pkg/controller/reconciler/client.go rename to pkg/controller/reconciler/radius_client.go index fba15025ab..744756d348 100644 --- a/pkg/controller/reconciler/client.go +++ b/pkg/controller/reconciler/radius_client.go @@ -1,5 +1,5 @@ /* -Copyright 2023. +Copyright 2024 The Radius Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,9 +18,7 @@ package reconciler import ( "context" - "net/http" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" @@ -29,15 +27,6 @@ import ( "github.com/radius-project/radius/pkg/ucp/resources" ) -type Poller[T any] interface { - Done() bool - Poll(ctx context.Context) (*http.Response, error) - Result(ctx context.Context) (T, error) - ResumeToken() (string, error) -} - -var _ Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse] = (*runtime.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse])(nil) - type RadiusClient interface { Applications(scope string) ApplicationClient Containers(scope string) ContainerClient @@ -78,17 +67,17 @@ type ResourceClient interface { ListSecrets(ctx context.Context, resourceName string) (generated.GenericResourcesClientListSecretsResponse, error) } -type Client struct { +type RadiusClientImpl struct { connection sdk.Connection } -func NewClient(connection sdk.Connection) *Client { - return &Client{connection: connection} +func NewRadiusClient(connection sdk.Connection) *RadiusClientImpl { + return &RadiusClientImpl{connection: connection} } -var _ RadiusClient = (*Client)(nil) +var _ RadiusClient = (*RadiusClientImpl)(nil) -func (c *Client) Applications(scope string) ApplicationClient { +func (c *RadiusClientImpl) Applications(scope string) ApplicationClient { ac, err := corerpv20231001preview.NewApplicationsClient(scope, &aztoken.AnonymousCredential{}, sdk.NewClientOptions(c.connection)) if err != nil { panic("failed to create client: " + err.Error()) @@ -97,7 +86,7 @@ func (c *Client) Applications(scope string) ApplicationClient { return &ApplicationClientImpl{inner: ac} } -func (c *Client) Containers(scope string) ContainerClient { +func (c *RadiusClientImpl) Containers(scope string) ContainerClient { cc, err := corerpv20231001preview.NewContainersClient(scope, &aztoken.AnonymousCredential{}, sdk.NewClientOptions(c.connection)) if err != nil { panic("failed to create client: " + err.Error()) @@ -106,7 +95,7 @@ func (c *Client) Containers(scope string) ContainerClient { return &ContainerClientImpl{inner: cc} } -func (c *Client) Environments(scope string) EnvironmentClient { +func (c *RadiusClientImpl) Environments(scope string) EnvironmentClient { ec, err := corerpv20231001preview.NewEnvironmentsClient(scope, &aztoken.AnonymousCredential{}, sdk.NewClientOptions(c.connection)) if err != nil { panic("failed to create client: " + err.Error()) @@ -115,7 +104,7 @@ func (c *Client) Environments(scope string) EnvironmentClient { return &EnvironmentClientImpl{inner: ec} } -func (c *Client) Groups(scope string) ResourceGroupClient { +func (c *RadiusClientImpl) Groups(scope string) ResourceGroupClient { rgc, err := ucpv20231001preview.NewResourceGroupsClient(&aztoken.AnonymousCredential{}, sdk.NewClientOptions(c.connection)) if err != nil { panic("failed to create client: " + err.Error()) @@ -124,7 +113,7 @@ func (c *Client) Groups(scope string) ResourceGroupClient { return &ResourceGroupClientImpl{inner: rgc, scope: scope} } -func (c *Client) Resources(scope string, resourceType string) ResourceClient { +func (c *RadiusClientImpl) Resources(scope string, resourceType string) ResourceClient { gc, err := generated.NewGenericResourcesClient(scope, resourceType, &aztoken.AnonymousCredential{}, sdk.NewClientOptions(c.connection)) if err != nil { panic("failed to create client: " + err.Error()) diff --git a/pkg/controller/service.go b/pkg/controller/service.go index bd9a381f8f..8c2094c370 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -93,7 +93,7 @@ func (s *Service) Run(ctx context.Context) error { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), EventRecorder: mgr.GetEventRecorderFor("recipe-controller"), - Radius: reconciler.NewClient(s.Options.UCPConnection), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "Recipe", err) @@ -102,25 +102,27 @@ func (s *Service) Run(ctx context.Context) error { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), EventRecorder: mgr.GetEventRecorderFor("radius-deployment-controller"), - Radius: reconciler.NewClient(s.Options.UCPConnection), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "Deployment", err) } err = (&reconciler.DeploymentTemplateReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), - Radius: reconciler.NewClient(s.Options.UCPConnection), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), + DeploymentClient: reconciler.NewDeploymentClient(s.Options.UCPConnection), }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentTemplate", err) } err = (&reconciler.DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: reconciler.NewClient(s.Options.UCPConnection), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), + DeploymentClient: reconciler.NewDeploymentClient(s.Options.UCPConnection), }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentResource", err) diff --git a/pkg/recipes/controllerconfig/config.go b/pkg/recipes/controllerconfig/config.go index 6ae1e3a82d..dbdf1afe59 100644 --- a/pkg/recipes/controllerconfig/config.go +++ b/pkg/recipes/controllerconfig/config.go @@ -44,7 +44,7 @@ type RecipeControllerConfig struct { ConfigLoader configloader.ConfigurationLoader // DeploymentEngineClient is the client for interacting with the deployment engine. - DeploymentEngineClient *clients.ResourceDeploymentsClient + DeploymentEngineClient clients.ResourceDeploymentsClient // Engine is the engine for executing recipes. Engine engine.Engine diff --git a/pkg/recipes/driver/bicep.go b/pkg/recipes/driver/bicep.go index 745d8b4d1c..3a60341bd7 100644 --- a/pkg/recipes/driver/bicep.go +++ b/pkg/recipes/driver/bicep.go @@ -56,7 +56,7 @@ const ( var _ Driver = (*bicepDriver)(nil) // NewBicepDriver creates a new bicep driver instance with the given ARM client options, deployment client, resource client, and options. -func NewBicepDriver(armOptions *arm.ClientOptions, deploymentClient *clients.ResourceDeploymentsClient, client processors.ResourceClient, options BicepOptions) Driver { +func NewBicepDriver(armOptions *arm.ClientOptions, deploymentClient clients.ResourceDeploymentsClient, client processors.ResourceClient, options BicepOptions) Driver { return &bicepDriver{ ArmClientOptions: armOptions, DeploymentClient: deploymentClient, @@ -72,7 +72,7 @@ type BicepOptions struct { type bicepDriver struct { ArmClientOptions *arm.ClientOptions - DeploymentClient *clients.ResourceDeploymentsClient + DeploymentClient clients.ResourceDeploymentsClient ResourceClient processors.ResourceClient options BicepOptions diff --git a/pkg/sdk/clients/resourcedeploymentsclient.go b/pkg/sdk/clients/resourcedeploymentsclient.go index 1cc67d363d..9552575836 100644 --- a/pkg/sdk/clients/resourcedeploymentsclient.go +++ b/pkg/sdk/clients/resourcedeploymentsclient.go @@ -94,15 +94,24 @@ type ProviderConfig struct { // ResourceDeploymentsClient is a deployments client for Azure Resource Manager. // It is used by both Azure and UCP clients. -type ResourceDeploymentsClient struct { +type ResourceDeploymentsClient interface { + CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) + ContinueCreateOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) + Delete(ctx context.Context, resourceID, apiVersion string) (*runtime.Poller[ClientDeleteResponse], error) + ContinueDeleteOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientDeleteResponse], error) +} + +type ResourceDeploymentsClientImpl struct { client *armresources.Client pipeline *runtime.Pipeline baseURI string } +var _ ResourceDeploymentsClient = (*ResourceDeploymentsClientImpl)(nil) + // NewResourceDeploymentsClient creates a new ResourceDeploymentsClient with the provided options and returns an error if // the options are invalid. -func NewResourceDeploymentsClient(options *Options) (*ResourceDeploymentsClient, error) { +func NewResourceDeploymentsClient(options *Options) (ResourceDeploymentsClient, error) { if options.BaseURI == "" { return nil, errors.New("baseURI cannot be empty") } @@ -118,7 +127,7 @@ func NewResourceDeploymentsClient(options *Options) (*ResourceDeploymentsClient, return nil, err } - return &ResourceDeploymentsClient{ + return &ResourceDeploymentsClientImpl{ client: client, pipeline: &pipeline, baseURI: options.BaseURI, @@ -130,9 +139,13 @@ type ClientCreateOrUpdateResponse struct { armresources.DeploymentExtended } +type ClientDeleteResponse struct { + armresources.DeploymentExtended +} + // CreateOrUpdate creates a request to create or update a deployment and returns a poller to // track the progress of the operation. -func (client *ResourceDeploymentsClient) CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) { +func (client *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) { if !strings.HasPrefix(resourceID, "/") { return nil, fmt.Errorf("error creating or updating a deployment: resourceID must start with a slash") } @@ -159,7 +172,7 @@ func (client *ResourceDeploymentsClient) CreateOrUpdate(ctx context.Context, par } // createOrUpdateCreateRequest creates the CreateOrUpdate request. -func (client *ResourceDeploymentsClient) createOrUpdateCreateRequest(ctx context.Context, resourceID, apiVersion string, parameters Deployment) (*policy.Request, error) { +func (client *ResourceDeploymentsClientImpl) createOrUpdateCreateRequest(ctx context.Context, resourceID, apiVersion string, parameters Deployment) (*policy.Request, error) { if resourceID == "" { return nil, errors.New("resourceID cannot be empty") } @@ -175,3 +188,57 @@ func (client *ResourceDeploymentsClient) createOrUpdateCreateRequest(ctx context req.Raw().Header["Accept"] = []string{"application/json"} return req, runtime.MarshalAsJSON(req, parameters) } + +// ContinueCreateOperation continues a create operation given a resume token. +func (client *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) { + return runtime.NewPollerFromResumeToken[ClientCreateOrUpdateResponse](resumeToken, *client.pipeline, nil) +} + +func (client *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (*runtime.Poller[ClientDeleteResponse], error) { + if !strings.HasPrefix(resourceID, "/") { + return nil, fmt.Errorf("error creating or updating a deployment: resourceID must start with a slash") + } + + _, err := resources.ParseResource(resourceID) + if err != nil { + return nil, fmt.Errorf("invalid resourceID: %v", resourceID) + } + + req, err := client.deleteCreateRequest(ctx, resourceID, apiVersion) + if err != nil { + return nil, err + } + + resp, err := client.pipeline.Do(req) + if err != nil { + return nil, err + } + if !runtime.HasStatusCode(resp, http.StatusOK, http.StatusNoContent, http.StatusAccepted, http.StatusNotFound) { + return nil, runtime.NewResponseError(resp) + } + + return runtime.NewPoller[ClientDeleteResponse](resp, *client.pipeline, nil) +} + +// deleteCreateRequest creates the Delete request. +func (client *ResourceDeploymentsClientImpl) deleteCreateRequest(ctx context.Context, resourceID, apiVersion string) (*policy.Request, error) { + if resourceID == "" { + return nil, errors.New("resourceID cannot be empty") + } + + urlPath := DeploymentEngineURL(client.baseURI, resourceID) + req, err := runtime.NewRequest(ctx, http.MethodDelete, urlPath) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", apiVersion) + req.Raw().URL.RawQuery = reqQP.Encode() + req.Raw().Header["Accept"] = []string{"application/json"} + return req, nil +} + +// ContinueCreateOperation continues a create operation given a resume token. +func (client *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientDeleteResponse], error) { + return runtime.NewPollerFromResumeToken[ClientDeleteResponse](resumeToken, *client.pipeline, nil) +} diff --git a/pkg/ucp/frontend/controller/resourcegroups/util.go b/pkg/ucp/frontend/controller/resourcegroups/util.go index bf4a975f2a..5d41849821 100644 --- a/pkg/ucp/frontend/controller/resourcegroups/util.go +++ b/pkg/ucp/frontend/controller/resourcegroups/util.go @@ -174,7 +174,6 @@ func ValidateResourceType(ctx context.Context, client database.Client, id resour } _, ok := locationResourceType.APIVersions[apiVersion] - fmt.Println("DEBUG - locationResourceType.APIVersions[apiVersion]: ", locationResourceType.APIVersions[apiVersion]) if !ok { return nil, &InvalidError{Message: fmt.Sprintf("api version %q is not supported for resource type %q by location %q", apiVersion, id.Type(), locationName)} } From 6e63cc4c943816dadee5ea447ab5d9d25e8b87b9 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sat, 25 Jan 2025 11:53:32 -0800 Subject: [PATCH 57/65] more unit tests Signed-off-by: willdavsmith --- .../deploymentresource_reconciler_test.go | 39 +- .../deploymenttemplate_reconciler_test.go | 563 ++++++++++++------ .../deploymenttemplate-outputresources-1.json | 66 ++ .../deploymenttemplate-outputresources-2.json | 50 ++ 4 files changed, 522 insertions(+), 196 deletions(-) create mode 100644 pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json create mode 100644 pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index 1327bd5605..bedfc83c04 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -30,7 +30,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" + k8sClient "sigs.k8s.io/controller-runtime/pkg/client" crconfig "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/metrics/server" ) @@ -50,7 +50,7 @@ var ( TestDeploymentResourceID = fmt.Sprintf("%s/providers/Microsoft.Resources/deployments/%s", TestDeploymentResourceScope, TestDeploymentResourceName) ) -func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client) { +func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, *mockDeploymentClient, k8sClient.Client) { SkipWithoutEnvironment(t) // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail @@ -77,13 +77,16 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client }) require.NoError(t, err) - radius := NewMockRadiusClient() + mockRadiusClient := NewMockRadiusClient() + mockDeploymentClient := NewMockDeploymentClient() + err = (&DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: radius, - DelayInterval: DeploymentResourceTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: mockRadiusClient, + DeploymentClient: mockDeploymentClient, + DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) @@ -92,33 +95,33 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, client.Client require.NoError(t, err) }() - return radius, mgr.GetClient() + return mockRadiusClient, mockDeploymentClient, mgr.GetClient() } func Test_DeploymentResourceReconciler_Basic(t *testing.T) { ctx := testcontext.New(t) - _, client := SetupDeploymentResourceTest(t) + _, _, k8sClient := SetupDeploymentTemplateTest(t) name := types.NamespacedName{Namespace: TestDeploymentResourceNamespace, Name: TestDeploymentResourceName} - err := client.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) + err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: name.Namespace}}) require.NoError(t, err) deployment := makeDeploymentResource(name, TestDeploymentResourceID) - err = client.Create(ctx, deployment) + err = k8sClient.Create(ctx, deployment) require.NoError(t, err) // Deployment will update after operation completes - status := waitForDeploymentResourceStateReady(t, client, name) + status := waitForDeploymentResourceStateReady(t, k8sClient, name) require.Equal(t, TestDeploymentResourceID, status.Id) - err = client.Delete(ctx, deployment) + err = k8sClient.Delete(ctx, deployment) require.NoError(t, err) // Now deleting of the DeploymentResource object can complete. - waitForDeploymentResourceDeleted(t, client, name) + waitForDeploymentResourceDeleted(t, k8sClient, name) } -func waitForDeploymentResourceStateReady(t *testing.T, client client.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentResourceStatus { +func waitForDeploymentResourceStateReady(t *testing.T, client k8sClient.Client, name types.NamespacedName) *radappiov1alpha3.DeploymentResourceStatus { ctx := testcontext.New(t) logger := t @@ -139,7 +142,7 @@ func waitForDeploymentResourceStateReady(t *testing.T, client client.Client, nam return status } -func waitForDeploymentResourceStateDeleting(t *testing.T, client client.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentResourceStatus { +func waitForDeploymentResourceStateDeleting(t *testing.T, client k8sClient.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentResourceStatus { ctx := testcontext.New(t) logger := t @@ -163,7 +166,7 @@ func waitForDeploymentResourceStateDeleting(t *testing.T, client client.Client, return status } -func waitForDeploymentResourceDeleted(t *testing.T, client client.Client, name types.NamespacedName) { +func waitForDeploymentResourceDeleted(t *testing.T, client k8sClient.Client, name types.NamespacedName) { ctx := testcontext.New(t) logger := t diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index 7c9b6e2843..a59a9279d8 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -225,44 +225,55 @@ func Test_DeploymentTemplateReconciler_IsUpToDate(t *testing.T) { } func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { + // This test tests the basic functionality of the DeploymentTemplate controller. + // It creates a DeploymentTemplate (with an empty template field), + // waits for it to be ready, and then deletes it. + // + // This is the same structure as all of the following tests. + ctx := testcontext.New(t) - _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-basic", Name: "test-deploymenttemplate-basic"} - err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) + // Set up the test. + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + testNamespace := "deploymenttemplate-basic" + testName := "test-deploymenttemplate-basic" + template := "{}" + parameters := map[string]string{} + providerConfig, err := sdkclients.NewDefaultProviderConfig(testNamespace).String() require.NoError(t, err) - providerConfig, err := sdkclients.NewDefaultProviderConfig("deploymenttemplate-basic").String() + // Create k8s namespace for the test. + namespacedName := types.NamespacedName{Namespace: testNamespace, Name: testName} + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: testNamespace}}) require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(namespacedName, "{}", providerConfig, map[string]string{}) + // Create the DeploymentTemplate resource. + deploymentTemplate := makeDeploymentTemplate(namespacedName, template, providerConfig, parameters) err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) - // Wait for the DeploymentTemplate to enter the updating state. + // Wait for the DeploymentTemplate to enter the Updating state. status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation. mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, nil) - // DeploymentTemplate should be ready after the operation completes. + // DeploymentTemplate should be Ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - // Verify that the DeploymentTemplate contains the expected properties. + // Verify that the DeploymentTemplate desired state contains the expected properties. expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: "{}", - Parameters: map[string]string{}, + Template: template, + Parameters: parameters, ProviderConfig: providerConfig, }, } - expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) require.NoError(t, err) - require.Equal(t, expectedStatusHash, status.StatusHash) - // Delete the DeploymentTemplate + // Trigger deletion of the DeploymentTemplate. err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) @@ -277,20 +288,27 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { // and verify that the controller will (eventually) retry these operations. ctx := testcontext.New(t) - _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-failurerecovery", Name: "test-deploymenttemplate-failurerecovery"} - err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) + // Set up the test. + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + testNamespace := "deploymenttemplate-failurerecovery" + testName := "test-deploymenttemplate-failurerecovery" + template := "{}" + parameters := map[string]string{} + providerConfig, err := sdkclients.NewDefaultProviderConfig(testNamespace).String() require.NoError(t, err) - providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-failurerecovery", "", "").String() + // Create k8s namespace for the test. + namespacedName := types.NamespacedName{Namespace: testNamespace, Name: testName} + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: testNamespace}}) require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(namespacedName, "{}", providerConfig, map[string]string{}) + // Create the DeploymentTemplate resource. + deploymentTemplate := makeDeploymentTemplate(namespacedName, template, providerConfig, parameters) err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) - // Wait for the DeploymentTemplate to enter the updating state. + // Wait for the DeploymentTemplate to enter the Updating state. status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation, but make it fail. @@ -310,46 +328,140 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { // Complete the operation, successfully this time. mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, nil) - _ = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - // Delete the DeploymentTemplate + // Verify that the DeploymentTemplate desired state contains the expected properties. + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + }, + } + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Trigger deletion of the DeploymentTemplate. err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) + // Wait for the DeploymentTemplate to be deleted. waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) } func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { + // This test tests the ability to handle deployments of + // resources created by the DeploymentTemplate. + ctx := testcontext.New(t) + + // Set up the test. _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + testNamespace := "deploymenttemplate-withresources" + testName := "test-deploymenttemplate-withresources" + template := readFileIntoTemplate(t, "deploymenttemplate-withresources.json") + parameters := map[string]string{} + providerConfig, err := sdkclients.NewDefaultProviderConfig(testNamespace).String() + require.NoError(t, err) - namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-withresources", Name: "test-deploymenttemplate-withresources"} - err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) + // Create k8s namespace for the test. + namespacedName := types.NamespacedName{Namespace: testNamespace, Name: testName} + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: testNamespace}}) require.NoError(t, err) - fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-withresources.json")) + // Create the DeploymentTemplate resource. + deploymentTemplate := makeDeploymentTemplate(namespacedName, template, providerConfig, parameters) + err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) - templateMap := map[string]any{} - err = json.Unmarshal(fileContent, &templateMap) + + // Wait for the DeploymentTemplate to enter the Updating state. + status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + + // Complete the operation. + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties.OutputResources = []*armresources.ResourceReference{ + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-withresources-env")}, + } + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + }) + + // DeploymentTemplate should be ready after the operation completes. + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + + // The dependencies (DeploymentResource resources) should be created. + dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-withresources-env"} + dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) + + // Verify that the DeploymentTemplate desired state contains the expected properties. + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + }, + } + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) require.NoError(t, err) - template, err := json.MarshalIndent(templateMap, "", " ") + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Trigger deletion of the DeploymentTemplate. + err = k8sClient.Delete(ctx, deploymentTemplate) require.NoError(t, err) - providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-withresources", "", "").String() + // The DeploymentTemplate should be in the deleting state. + waitForDeploymentTemplateStateDeleting(t, k8sClient, namespacedName) + + // Get the status of the dependency (DeploymentResource resource). + dependencyStatus = waitForDeploymentResourceStateDeleting(t, k8sClient, dependencyName, nil) + + // Complete the delete operation on the DeploymentResource. + mockDeploymentClient.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) + + // Wait for the DeploymentTemplate to be deleted. + waitForDeploymentResourceDeleted(t, k8sClient, dependencyName) + waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) +} + +func Test_DeploymentTemplateReconciler_Update(t *testing.T) { + // This test tests our ability to update a DeploymentTemplate. + // We create a DeploymentTemplate, update it, and verify that the Radius resource is updated accordingly. + + ctx := testcontext.New(t) + + // Set up the test. + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + testNamespace := "deploymenttemplate-update" + testName := "test-deploymenttemplate-update" + template := readFileIntoTemplate(t, "deploymenttemplate-update-1.json") + parameters := map[string]string{} + providerConfig, err := sdkclients.NewDefaultProviderConfig(testNamespace).String() require.NoError(t, err) - deploymentTemplate := makeDeploymentTemplate(namespacedName, string(template), providerConfig, map[string]string{}) + // Create k8s namespace for the test. + namespacedName := types.NamespacedName{Namespace: testNamespace, Name: testName} + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: testNamespace}}) + require.NoError(t, err) + + // Create the DeploymentTemplate resource. + deploymentTemplate := makeDeploymentTemplate(namespacedName, template, providerConfig, parameters) err = k8sClient.Create(ctx, deploymentTemplate) require.NoError(t, err) + // Wait for the DeploymentTemplate to enter the Updating state. status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + // Complete the operation. mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ - {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env")}, + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env")}, } state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) @@ -357,20 +469,70 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { // DeploymentTemplate should be ready after the operation completes. status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-withresources-env"} + // The dependencies (DeploymentResource resources) should be created. + dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) - require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-withresources/providers/Applications.Core/environments/deploymenttemplate-withresources-env", dependencyStatus.Id) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + // Verify that the DeploymentTemplate desired state contains the expected properties. expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ Spec: radappiov1alpha3.DeploymentTemplateSpec{ - Template: string(template), - Parameters: map[string]string{}, + Template: template, + Parameters: parameters, ProviderConfig: providerConfig, }, } expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Now, we will re-deploy the DeploymentTemplate with a new template. + + // Get the DeploymentTemplate resource. + newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} + err = k8sClient.Get(ctx, namespacedName, &newDeploymentTemplate) + require.NoError(t, err) + + // Update the template field on the DeploymentTemplate. + template = readFileIntoTemplate(t, "deploymenttemplate-update-2.json") + newDeploymentTemplate.Spec.Template = string(template) + + // Update the DeploymentTemplate resource. + err = k8sClient.Update(ctx, &newDeploymentTemplate) + require.NoError(t, err) + + // Now, the DeploymentTemplate should re-enter the Updating state. + status = waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + + // Complete the operation again. + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + require.True(t, ok, "failed to find resource") + resource.Properties.OutputResources = []*armresources.ResourceReference{ + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env")}, + } + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + }) + + // DeploymentTemplate should be Ready again after the operation completes. + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + + // The dependencies (DeploymentResource resources) should also be Ready. + dependencyName = types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} + dependencyStatus = waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) + require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) + + // Verify that the DeploymentTemplate contains the expected properties. + expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + }, + } + expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) require.Equal(t, expectedStatusHash, status.StatusHash) // Trigger deletion of the DeploymentTemplate. @@ -386,154 +548,188 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { // Complete the delete operation on the DeploymentResource. mockDeploymentClient.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) + // Wait for the DeploymentTemplate to be deleted. waitForDeploymentResourceDeleted(t, k8sClient, dependencyName) waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) } -// func Test_DeploymentTemplateReconciler_Update(t *testing.T) { -// // This test tests our ability to update a DeploymentTemplate. -// // We create a DeploymentTemplate, update it, and verify that the Radius resource is updated accordingly. - -// ctx := testcontext.New(t) -// _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) - -// namespacedName := types.NamespacedName{Namespace: "deploymenttemplate-update", Name: "test-deploymenttemplate-update"} -// err := k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: namespacedName.Namespace}}) -// require.NoError(t, err) - -// fileContent, err := os.ReadFile(path.Join("testdata", "deploymenttemplate-update-1.json")) -// require.NoError(t, err) -// templateMap := map[string]any{} -// err = json.Unmarshal(fileContent, &templateMap) -// require.NoError(t, err) -// template, err := json.MarshalIndent(templateMap, "", " ") -// require.NoError(t, err) - -// scope := "/planes/radius/local/resourceGroups/deploymenttemplate-update" -// providerConfig, err := sdkclients.GenerateProviderConfig("deploymenttemplate-update", "", "").String() -// require.NoError(t, err) - -// deploymentTemplate := makeDeploymentTemplate(namespacedName, string(template), providerConfig, map[string]string{}) -// err = k8sClient.Create(ctx, deploymentTemplate) -// require.NoError(t, err) - -// status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) - -// radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { -// resource, ok := radius.resources[state.resourceID] -// require.True(t, ok, "failed to find resource") - -// resource.Properties["outputResources"] = []any{ -// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, -// } -// state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} -// }) - -// // DeploymentTemplate should be ready after the operation completes. -// status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - -// // DeploymentTemplate will be waiting for environment to be created. -// createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") - -// dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} -// dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) -// require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) - -// // Verify that the Radius deployment contains the expected properties. -// resource, err := radius.Resources(scope, "Microsoft.Resources/deployments").Get(ctx, namespacedName.Name) -// require.NoError(t, err) -// expectedProperties := map[string]any{ -// "mode": "Incremental", -// "template": templateMap, -// "parameters": map[string]map[string]string{}, -// "providerConfig": sdkclients.ProviderConfig{ -// Radius: &sdkclients.Radius{ -// Type: "Radius", -// Value: sdkclients.Value{ -// Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", -// }, -// }, -// Deployments: &sdkclients.Deployments{ -// Type: "Microsoft.Resources", -// Value: sdkclients.Value{ -// Scope: "/planes/radius/local/resourceGroups/deploymenttemplate-update", -// }, -// }, -// }, -// "outputResources": []any{ -// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, -// }, -// } -// require.Equal(t, expectedProperties, resource.Properties) - -// // Verify that the DeploymentTemplate contains the expected properties. -// expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ -// Spec: radappiov1alpha3.DeploymentTemplateSpec{ -// Template: string(template), -// Parameters: map[string]string{}, -// ProviderConfig: providerConfig, -// }, -// } - -// expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) -// require.NoError(t, err) -// require.Equal(t, expectedStatusHash, status.StatusHash) - -// // Re-deploy the DeploymentTemplate with a new template. - -// fileContent, err = os.ReadFile(path.Join("testdata", "deploymenttemplate-update-2.json")) -// require.NoError(t, err) -// templateMap = map[string]any{} -// err = json.Unmarshal(fileContent, &templateMap) -// require.NoError(t, err) -// template, err = json.MarshalIndent(templateMap, "", " ") -// require.NoError(t, err) - -// newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} -// err = k8sClient.Get(ctx, namespacedName, &newDeploymentTemplate) -// require.NoError(t, err) - -// // Update the template -// newDeploymentTemplate.Spec.Template = string(template) - -// err = k8sClient.Update(ctx, &newDeploymentTemplate) -// require.NoError(t, err) - -// status = waitForDeploymentTemplateStateUpdating(t, k8sClient, name, nil) - -// mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { -// resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] -// require.True(t, ok, "failed to find resource") - -// resource.Properties["outputResources"] = []any{ -// map[string]any{"id": "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env"}, -// } -// state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} -// }) - -// // DeploymentTemplate should be ready after the operation completes. -// status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) - -// // DeploymentTemplate will be waiting for environment to be created. -// createEnvironment(radius, "deploymenttemplate-update", "deploymenttemplate-update-env") - -// dependencyName = types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-update-env"} -// dependencyStatus = waitForDeploymentResourceStateReady(t, k8sClient, dependencyName) -// require.Equal(t, "/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env", dependencyStatus.Id) - -// // Verify that the DeploymentTemplate contains the expected properties. -// expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ -// Spec: radappiov1alpha3.DeploymentTemplateSpec{ -// Template: string(template), -// Parameters: map[string]string{}, -// ProviderConfig: providerConfig, -// }, -// } - -// expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) -// require.NoError(t, err) -// require.Equal(t, expectedStatusHash, status.StatusHash) -// } +func Test_DeploymentTemplateReconciler_OutputResources(t *testing.T) { + // This test tests the ability to perform diff detection on + // the OutputResources field of the DeploymentTemplate. + // We create a DeploymentTemplate with some resources, + // update the DeploymentTemplate to remove some resources, + // and verify that the diff is correct. + + ctx := testcontext.New(t) + + // Set up the test. + _, mockDeploymentClient, k8sClient := SetupDeploymentTemplateTest(t) + testNamespace := "deploymenttemplate-outputresources" + testName := "test-deploymenttemplate-outputresources" + template := readFileIntoTemplate(t, "deploymenttemplate-outputresources-1.json") + parameters := map[string]string{} + providerConfig, err := sdkclients.NewDefaultProviderConfig(testNamespace).String() + require.NoError(t, err) + + // Create k8s namespace for the test. + namespacedName := types.NamespacedName{Namespace: testNamespace, Name: testName} + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: ctrl.ObjectMeta{Name: testNamespace}}) + require.NoError(t, err) + + // Create the DeploymentTemplate resource. + deploymentTemplate := makeDeploymentTemplate(namespacedName, template, providerConfig, parameters) + err = k8sClient.Create(ctx, deploymentTemplate) + require.NoError(t, err) + + // Wait for the DeploymentTemplate to enter the Updating state. + status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + + // Complete the operation. + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties.OutputResources = []*armresources.ResourceReference{ + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/environments/deploymenttemplate-outputresources-environment")}, + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application")}, + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/containers/deploymenttemplate-outputresources-container")}, + } + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + }) + + // DeploymentTemplate should be ready after the operation completes. + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + + // The dependencies (DeploymentResource resources) should be created. + dependencies := []struct { + resourceID string + namespacedName types.NamespacedName + }{ + { + resourceID: "/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/environments/deploymenttemplate-outputresources-environment", + namespacedName: types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-environment"}, + }, + { + resourceID: "/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application", + namespacedName: types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-application"}, + }, + { + resourceID: "/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/containers/deploymenttemplate-outputresources-container", + namespacedName: types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-container"}, + }, + } + for _, dependency := range dependencies { + dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependency.namespacedName) + require.Equal(t, dependency.resourceID, dependencyStatus.Id) + } + + // Verify that the DeploymentTemplate desired state contains the expected properties. + expectedDeploymentTemplateSpec := &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: parameters, + ProviderConfig: providerConfig, + }, + } + expectedStatusHash, err := computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Now, we will re-deploy the DeploymentTemplate with a new template. + // This template has the container resource removed. + + // Get the DeploymentTemplate resource. + newDeploymentTemplate := radappiov1alpha3.DeploymentTemplate{} + err = k8sClient.Get(ctx, namespacedName, &newDeploymentTemplate) + require.NoError(t, err) + + // Update the template field on the DeploymentTemplate. + template = readFileIntoTemplate(t, "deploymenttemplate-outputresources-2.json") + newDeploymentTemplate.Spec.Template = string(template) + + // Update the DeploymentTemplate resource. + err = k8sClient.Update(ctx, &newDeploymentTemplate) + require.NoError(t, err) + + // Now, the DeploymentTemplate should re-enter the Updating state. + status = waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) + + // Complete the operation again, with a different set of output resources. + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { + resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + require.True(t, ok, "failed to find resource") + + resource.Properties.OutputResources = []*armresources.ResourceReference{ + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/environments/deploymenttemplate-outputresources-environment")}, + {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application")}, + } + state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + }) + + // Complete the delete operation on the container resource. + dependencyName := types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-container"} + dependencyStatus := waitForDeploymentResourceStateDeleting(t, k8sClient, dependencyName, nil) + mockDeploymentClient.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) + + // DeploymentTemplate should be Ready again after the operation completes. + status = waitForDeploymentTemplateStateReady(t, k8sClient, namespacedName) + + // The dependencies (DeploymentResource resources) should be in the Ready state. + dependencies = []struct { + resourceID string + namespacedName types.NamespacedName + }{ + { + resourceID: "/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/environments/deploymenttemplate-outputresources-environment", + namespacedName: types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-environment"}, + }, + { + resourceID: "/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application", + namespacedName: types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-application"}, + }, + } + for _, dependency := range dependencies { + dependencyStatus := waitForDeploymentResourceStateReady(t, k8sClient, dependency.namespacedName) + require.Equal(t, dependency.resourceID, dependencyStatus.Id) + } + + // Assert that the container resource has been deleted. + dependencyName = types.NamespacedName{Namespace: namespacedName.Namespace, Name: "deploymenttemplate-outputresources-container"} + waitForDeploymentResourceDeleted(t, k8sClient, dependencyName) + + // Check the cluster for the container resource, it should not exist. + err = k8sClient.Get(ctx, dependencyName, &radappiov1alpha3.DeploymentResource{}) + require.True(t, apierrors.IsNotFound(err), "expected DeploymentResource to be deleted") + + // Verify that the DeploymentTemplate contains the expected properties. + expectedDeploymentTemplateSpec = &radappiov1alpha3.DeploymentTemplate{ + Spec: radappiov1alpha3.DeploymentTemplateSpec{ + Template: template, + Parameters: map[string]string{}, + ProviderConfig: providerConfig, + }, + } + expectedStatusHash, err = computeHash(expectedDeploymentTemplateSpec) + require.NoError(t, err) + require.Equal(t, expectedStatusHash, status.StatusHash) + + // Trigger deletion of the DeploymentTemplate. + err = k8sClient.Delete(ctx, deploymentTemplate) + require.NoError(t, err) + + // The DeploymentTemplate should be in the deleting state. + waitForDeploymentTemplateStateDeleting(t, k8sClient, namespacedName) + + // Wait for all of the dependencies (DeploymentResource resources) to be deleted. + for _, dependency := range dependencies { + dependencyStatus := waitForDeploymentResourceStateDeleting(t, k8sClient, dependency.namespacedName, nil) + mockDeploymentClient.CompleteOperation(dependencyStatus.Operation.ResumeToken, nil) + waitForDeploymentResourceDeleted(t, k8sClient, dependency.namespacedName) + } + + // Wait for the DeploymentTemplate to be deleted. + waitForDeploymentTemplateStateDeleted(t, k8sClient, namespacedName) +} func waitForDeploymentTemplateStateUpdating(t *testing.T, client k8sclient.Client, name types.NamespacedName, oldOperation *radappiov1alpha3.ResourceOperation) *radappiov1alpha3.DeploymentTemplateStatus { ctx := testcontext.New(t) @@ -625,3 +821,14 @@ func waitForDeploymentTemplateStateDeleted(t *testing.T, client k8sclient.Client }, deploymentTemplateTestWaitDuration, deploymentTemplateTestWaitInterval, "DeploymentTemplate still exists") } + +func readFileIntoTemplate(t *testing.T, filename string) string { + fileContent, err := os.ReadFile(path.Join("testdata", filename)) + require.NoError(t, err) + templateMap := map[string]any{} + err = json.Unmarshal(fileContent, &templateMap) + require.NoError(t, err) + template, err := json.MarshalIndent(templateMap, "", " ") + require.NoError(t, err) + return string(template) +} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json new file mode 100644 index 0000000000..e1f07b9bf1 --- /dev/null +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.33.13.18514", + "templateHash": "2963919247542208354" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "outputResourcesEnvironment": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "output-resources-environment", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "default" + } + } + } + }, + "outputResourcesApplication": { + "import": "Radius", + "type": "Applications.Core/applications@2023-10-01-preview", + "properties": { + "name": "output-resources-application", + "properties": { + "environment": "[reference('outputResourcesEnvironment').id]" + } + }, + "dependsOn": [ + "outputResourcesEnvironment" + ] + }, + "outputResourcesContainer": { + "import": "Radius", + "type": "Applications.Core/containers@2023-10-01-preview", + "properties": { + "name": "output-resources-container", + "properties": { + "application": "[reference('outputResourcesApplication').id]", + "container": { + "image": "nginx" + } + } + }, + "dependsOn": [ + "outputResourcesApplication" + ] + } + } +} \ No newline at end of file diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json new file mode 100644 index 0000000000..ee0d772185 --- /dev/null +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.1-experimental", + "contentVersion": "1.0.0.0", + "metadata": { + "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", + "_EXPERIMENTAL_FEATURES_ENABLED": [ + "Extensibility" + ], + "_generator": { + "name": "bicep", + "version": "0.33.13.18514", + "templateHash": "2963919247542208354" + } + }, + "imports": { + "Radius": { + "provider": "Radius", + "version": "latest" + } + }, + "resources": { + "outputResourcesEnvironment": { + "import": "Radius", + "type": "Applications.Core/environments@2023-10-01-preview", + "properties": { + "name": "output-resources-environment", + "properties": { + "compute": { + "kind": "kubernetes", + "namespace": "default" + } + } + } + }, + "outputResourcesApplication": { + "import": "Radius", + "type": "Applications.Core/applications@2023-10-01-preview", + "properties": { + "name": "output-resources-application", + "properties": { + "environment": "[reference('outputResourcesEnvironment').id]" + } + }, + "dependsOn": [ + "outputResourcesEnvironment" + ] + } + } +} \ No newline at end of file From 32de24d836792564321fb3822f057ba953b5ebb0 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sat, 25 Jan 2025 12:01:43 -0800 Subject: [PATCH 58/65] lint Signed-off-by: willdavsmith --- bicep-types | 2 +- pkg/controller/reconciler/shared_test.go | 7 ------- .../deploymenttemplate-outputresources-1.json | 14 ++++---------- .../deploymenttemplate-outputresources-2.json | 10 +++------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/bicep-types b/bicep-types index 0143e0b634..3676a8bf68 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 0143e0b634b77515e681a57c5c5d594da8093825 +Subproject commit 3676a8bf689e62780c64c79bdca42f1799958cd4 diff --git a/pkg/controller/reconciler/shared_test.go b/pkg/controller/reconciler/shared_test.go index c680fca5d3..6990f7adbe 100644 --- a/pkg/controller/reconciler/shared_test.go +++ b/pkg/controller/reconciler/shared_test.go @@ -54,13 +54,6 @@ func createEnvironment(radius *mockRadiusClient, resourceGroup, name string) { }) } -func deleteEnvironment(radius *mockRadiusClient, resourceGroup, name string) { - id := fmt.Sprintf("/planes/radius/local/resourceGroups/%s/providers/Applications.Core/environments/%s", resourceGroup, name) - radius.Delete(func() { - delete(radius.environments, id) - }) -} - func makeRecipe(name types.NamespacedName, resourceType string) *radappiov1alpha3.Recipe { return &radappiov1alpha3.Recipe{ ObjectMeta: ctrl.ObjectMeta{ diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json index e1f07b9bf1..a556af9256 100644 --- a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-1.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.33.13.18514", @@ -42,9 +40,7 @@ "environment": "[reference('outputResourcesEnvironment').id]" } }, - "dependsOn": [ - "outputResourcesEnvironment" - ] + "dependsOn": ["outputResourcesEnvironment"] }, "outputResourcesContainer": { "import": "Radius", @@ -58,9 +54,7 @@ } } }, - "dependsOn": [ - "outputResourcesApplication" - ] + "dependsOn": ["outputResourcesApplication"] } } -} \ No newline at end of file +} diff --git a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json index ee0d772185..7fc4fbf488 100644 --- a/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json +++ b/pkg/controller/reconciler/testdata/deploymenttemplate-outputresources-2.json @@ -4,9 +4,7 @@ "contentVersion": "1.0.0.0", "metadata": { "_EXPERIMENTAL_WARNING": "This template uses ARM features that are experimental. Experimental features should be enabled for testing purposes only, as there are no guarantees about the quality or stability of these features. Do not enable these settings for any production usage, or your production environment may be subject to breaking.", - "_EXPERIMENTAL_FEATURES_ENABLED": [ - "Extensibility" - ], + "_EXPERIMENTAL_FEATURES_ENABLED": ["Extensibility"], "_generator": { "name": "bicep", "version": "0.33.13.18514", @@ -42,9 +40,7 @@ "environment": "[reference('outputResourcesEnvironment').id]" } }, - "dependsOn": [ - "outputResourcesEnvironment" - ] + "dependsOn": ["outputResourcesEnvironment"] } } -} \ No newline at end of file +} From f309cccef7ab141a4e6273d26ecc55cae2ea6f3d Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sun, 26 Jan 2025 17:32:52 -0800 Subject: [PATCH 59/65] PR Signed-off-by: willdavsmith --- .../radius/radapp.io_deploymentresources.yaml | 17 +++------ .../radius/radapp.io_deploymenttemplates.yaml | 18 ++++----- .../deploymentresource_reconciler.go | 6 +-- .../deploymenttemplate_reconciler_test.go | 37 +++++++++++++++++++ 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml index c042c2c09b..cea26a1b55 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymentresources.yaml @@ -46,24 +46,19 @@ spec: metadata: type: object spec: - description: DeploymentResourceSpec defines the desired state of DeploymentResource + description: DeploymentResourceSpec defines the desired state of a DeploymentResource + resource. properties: id: - description: Id is the resource Id. - type: string - providerConfig: - description: ProviderConfig specifies the scope for resources + description: Id is the resource id of the Radius resource. type: string type: object status: - description: DeploymentResourceStatus defines the observed state of DeploymentResource + description: DeploymentResourceStatus defines the observed state of a + DeploymentResource resource. properties: id: - description: Id is the resource Id. - type: string - message: - description: Message is a human-readable description of the status - of the Deployment Resource. + description: Id is the resource id of the Radius resource. type: string observedGeneration: description: ObservedGeneration is the most recent generation observed diff --git a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml index 81a6d66c07..e520a0a67e 100644 --- a/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml +++ b/deploy/Chart/crds/radius/radapp.io_deploymenttemplates.yaml @@ -46,7 +46,8 @@ spec: metadata: type: object spec: - description: DeploymentTemplateSpec defines the desired state of DeploymentTemplate + description: DeploymentTemplateSpec defines the desired state of a DeploymentTemplate + resource. properties: parameters: additionalProperties: @@ -54,7 +55,7 @@ spec: description: Parameters is the ARM JSON parameters for the template. type: object providerConfig: - description: ProviderConfig specifies the scope for resources + description: ProviderConfig specifies the scopes for resources. type: string template: description: Template is the ARM JSON manifest that defines the resources @@ -62,12 +63,9 @@ spec: type: string type: object status: - description: DeploymentTemplateStatus defines the observed state of DeploymentTemplate + description: DeploymentTemplateStatus defines the observed state of a + DeploymentTemplate resource. properties: - message: - description: Message is a human-readable description of the status - of the Deployment Template. - type: string observedGeneration: description: ObservedGeneration is the most recent generation observed for this DeploymentTemplate. @@ -96,11 +94,9 @@ spec: description: Phrase indicates the current status of the Deployment Template. type: string - resource: - description: Resource is the resource id of the deployment. - type: string statusHash: - description: StatusHash is a hash of the DeploymentTemplate's status. + description: StatusHash is a hash of the DeploymentTemplate's state + (template, parameters, and provider config). type: string type: object type: object diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index 7a481c36ea..a3bdebca0e 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -101,7 +101,6 @@ func (r *DeploymentResourceReconciler) Reconcile(ctx context.Context, req ctrl.R // NOTE: if reconcileOperation completes successfully, then it will return a "zero" result, // this means the operation has completed and we should continue processing. logger.Info("Operation completed successfully.") - // TODO (willsmith) return here? } else { logger.Info("Requeueing to continue operation.") return result, nil @@ -297,11 +296,10 @@ func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, logger := ucplog.FromContextOrDiscard(ctx) resourceId := deploymentResource.Spec.Id - // TODO (willsmith) HARDCODED API VERSION - apiVersion := "2023-10-01-preview" + radiusAPIVersion := "2023-10-01-preview" logger.Info("Starting DELETE operation.") - poller, err := r.DeploymentClient.ResourceDeployments().Delete(ctx, resourceId, apiVersion) + poller, err := r.DeploymentClient.ResourceDeployments().Delete(ctx, resourceId, radiusAPIVersion) if err != nil { return nil, err } else if poller != nil { diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index a59a9279d8..b84493d1a1 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -224,6 +224,43 @@ func Test_DeploymentTemplateReconciler_IsUpToDate(t *testing.T) { } } +func Test_ParseDeploymentScopeFromProviderConfig(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + providerConfig string + wantScope string + wantErr bool + }{ + { + name: "valid: provider with scope", + providerConfig: `{"deployments":{"type":"deployments","value":{"scope":"deploymentsscope"}}}`, + wantScope: "deploymentsscope", + wantErr: false, + }, + { + name: "invalid: deployments scope not present", + providerConfig: `{"radius":{"type":"radius","value":{"scope":"deploymentsscope"}}}`, + wantErr: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + scope, err := ParseDeploymentScopeFromProviderConfig(tc.providerConfig) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.wantScope, scope) + }) + } +} + func Test_DeploymentTemplateReconciler_Basic(t *testing.T) { // This test tests the basic functionality of the DeploymentTemplate controller. // It creates a DeploymentTemplate (with an empty template field), From 2bb3e04fee4d86ad50de2267e99cd655fd7869c3 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sun, 26 Jan 2025 20:53:10 -0800 Subject: [PATCH 60/65] biceptypes Signed-off-by: willdavsmith --- bicep-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicep-types b/bicep-types index 3676a8bf68..5f9dd8d3eb 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 3676a8bf689e62780c64c79bdca42f1799958cd4 +Subproject commit 5f9dd8d3ebdef4093c3d13b4be78b7059cae1296 From 645dff04ad290ae5c47df0324bc387b52de77b60 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sun, 26 Jan 2025 20:56:23 -0800 Subject: [PATCH 61/65] biceptypes Signed-off-by: willdavsmith --- bicep-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicep-types b/bicep-types index 5f9dd8d3eb..8dd60b7e6b 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 5f9dd8d3ebdef4093c3d13b4be78b7059cae1296 +Subproject commit 8dd60b7e6b87b290bd2738fa0ddd3fd289a3d3cb From 72de369155eeb9bcbcf79e9ca68a2cc994e34749 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Sun, 26 Jan 2025 20:57:03 -0800 Subject: [PATCH 62/65] biceptypes Signed-off-by: willdavsmith --- bicep-types | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bicep-types b/bicep-types index 8dd60b7e6b..0143e0b634 160000 --- a/bicep-types +++ b/bicep-types @@ -1 +1 @@ -Subproject commit 8dd60b7e6b87b290bd2738fa0ddd3fd289a3d3cb +Subproject commit 0143e0b634b77515e681a57c5c5d594da8093825 From cfa5f66ae9c5e2ddee891c035262c03b48173fb7 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 27 Jan 2025 14:24:50 -0800 Subject: [PATCH 63/65] PR Signed-off-by: willdavsmith --- pkg/controller/reconciler/deployment_client.go | 13 +++++++++++++ pkg/sdk/clients/resourcedeploymentsclient.go | 2 ++ 2 files changed, 15 insertions(+) diff --git a/pkg/controller/reconciler/deployment_client.go b/pkg/controller/reconciler/deployment_client.go index 4e3424933a..1cfbe7e28f 100644 --- a/pkg/controller/reconciler/deployment_client.go +++ b/pkg/controller/reconciler/deployment_client.go @@ -25,10 +25,14 @@ import ( sdkclients "github.com/radius-project/radius/pkg/sdk/clients" ) +// DeploymentClient is an interface for interacting with +// UCP ResourceDeploymentsClient. type DeploymentClient interface { ResourceDeployments() ResourceDeploymentsClient } +// ResourceDeploymentsClient is an interface for interacting +// with UCP Deployments. type ResourceDeploymentsClient interface { CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) @@ -36,38 +40,47 @@ type ResourceDeploymentsClient interface { ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) } +// DeploymentClientImpl is an implementation of DeploymentClient. type DeploymentClientImpl struct { connection sdk.Connection } +// NewDeploymentClient creates a new DeploymentClient +// with the given connection. func NewDeploymentClient(connection sdk.Connection) *DeploymentClientImpl { return &DeploymentClientImpl{connection: connection} } var _ DeploymentClient = (*DeploymentClientImpl)(nil) +// CreateOrUpdate creates or updates a deployment. func (rdc *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { return rdc.inner.CreateOrUpdate(ctx, parameters, resourceID, apiVersion) } +// ContinueCreateOperation continues a create operation. func (rdc *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { return rdc.inner.ContinueCreateOperation(ctx, resumeToken) } +// Delete deletes a deployment. func (rdc *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) { return rdc.inner.Delete(ctx, resourceID, apiVersion) } +// ContinueDeleteOperation continues a delete operation. func (rdc *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) { return rdc.inner.ContinueDeleteOperation(ctx, resumeToken) } var _ ResourceDeploymentsClient = (*ResourceDeploymentsClientImpl)(nil) +// ResourceDeploymentsClientImpl is an implementation of ResourceDeploymentsClient. type ResourceDeploymentsClientImpl struct { inner sdkclients.ResourceDeploymentsClient } +// ResourceDeployments returns a ResourceDeploymentsClient. func (c *DeploymentClientImpl) ResourceDeployments() ResourceDeploymentsClient { rdc, err := sdkclients.NewResourceDeploymentsClient(&sdkclients.Options{ Cred: &aztoken.AnonymousCredential{}, diff --git a/pkg/sdk/clients/resourcedeploymentsclient.go b/pkg/sdk/clients/resourcedeploymentsclient.go index 9552575836..15643964cd 100644 --- a/pkg/sdk/clients/resourcedeploymentsclient.go +++ b/pkg/sdk/clients/resourcedeploymentsclient.go @@ -194,6 +194,8 @@ func (client *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context return runtime.NewPollerFromResumeToken[ClientCreateOrUpdateResponse](resumeToken, *client.pipeline, nil) } +// Delete creates a request to delete a resource and returns a poller to +// track the progress of the operation. func (client *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (*runtime.Poller[ClientDeleteResponse], error) { if !strings.HasPrefix(resourceID, "/") { return nil, fmt.Errorf("error creating or updating a deployment: resourceID must start with a slash") From 6fc91ff59af71a029cd671d02edf78b8beaa8f29 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 27 Jan 2025 16:59:04 -0800 Subject: [PATCH 64/65] Unifying interfaces Signed-off-by: willdavsmith --- pkg/cli/deployment/deploy.go | 7 +- .../reconciler/deployment_client.go | 95 --------- .../reconciler/deployment_reconciler.go | 5 +- .../deploymentresource_reconciler.go | 10 +- .../deploymentresource_reconciler_test.go | 19 +- .../deploymenttemplate_reconciler.go | 10 +- .../deploymenttemplate_reconciler_test.go | 68 +++--- .../mock_deployments_client_test.go | 200 ------------------ .../reconciler/mock_radius_client_test.go | 72 +++---- pkg/controller/reconciler/radius_client.go | 33 +-- .../reconciler/recipe_reconciler.go | 5 +- .../reconciler/recipe_reconciler_test.go | 21 +- pkg/controller/reconciler/util.go | 9 +- pkg/controller/service.go | 29 ++- pkg/recipes/driver/bicep.go | 3 +- .../clients/mock_resourcedeploymentsclient.go | 198 +++++++++++++++++ .../reconciler => sdk/clients}/poller.go | 22 +- pkg/sdk/clients/resourcedeploymentsclient.go | 17 +- 18 files changed, 375 insertions(+), 448 deletions(-) delete mode 100644 pkg/controller/reconciler/deployment_client.go delete mode 100644 pkg/controller/reconciler/mock_deployments_client_test.go create mode 100644 pkg/sdk/clients/mock_resourcedeploymentsclient.go rename pkg/{controller/reconciler => sdk/clients}/poller.go (51%) diff --git a/pkg/cli/deployment/deploy.go b/pkg/cli/deployment/deploy.go index d0aa7c3380..c22b7e7404 100644 --- a/pkg/cli/deployment/deploy.go +++ b/pkg/cli/deployment/deploy.go @@ -24,7 +24,6 @@ import ( "sync" "time" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/google/uuid" v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" @@ -94,7 +93,7 @@ func (dc *ResourceDeploymentClient) Deploy(ctx context.Context, options clients. return summary, nil } -func (dc *ResourceDeploymentClient) startDeployment(ctx context.Context, name string, options clients.DeploymentOptions) (*runtime.Poller[sdkclients.ClientCreateOrUpdateResponse], error) { +func (dc *ResourceDeploymentClient) startDeployment(ctx context.Context, name string, options clients.DeploymentOptions) (sdkclients.Poller[sdkclients.ClientCreateOrUpdateResponse], error) { var resourceId string scopes := []ucpresources.ScopeSegment{ { @@ -197,8 +196,8 @@ func (dc *ResourceDeploymentClient) createSummary(deployment *armresources.Deplo return clients.DeploymentResult{Resources: resources, Outputs: outputs}, nil } -func (dc *ResourceDeploymentClient) waitForCompletion(ctx context.Context, poller *runtime.Poller[sdkclients.ClientCreateOrUpdateResponse]) (clients.DeploymentResult, error) { - resp, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: deploymentPollInterval}) +func (dc *ResourceDeploymentClient) waitForCompletion(ctx context.Context, poller sdkclients.Poller[sdkclients.ClientCreateOrUpdateResponse]) (clients.DeploymentResult, error) { + resp, err := poller.PollUntilDone(ctx, &sdkclients.PollUntilDoneOptions{Frequency: deploymentPollInterval}) if err != nil { return clients.DeploymentResult{}, err } diff --git a/pkg/controller/reconciler/deployment_client.go b/pkg/controller/reconciler/deployment_client.go deleted file mode 100644 index 1cfbe7e28f..0000000000 --- a/pkg/controller/reconciler/deployment_client.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2023. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconciler - -import ( - "context" - "fmt" - - aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" - "github.com/radius-project/radius/pkg/sdk" - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" -) - -// DeploymentClient is an interface for interacting with -// UCP ResourceDeploymentsClient. -type DeploymentClient interface { - ResourceDeployments() ResourceDeploymentsClient -} - -// ResourceDeploymentsClient is an interface for interacting -// with UCP Deployments. -type ResourceDeploymentsClient interface { - CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) - ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) - Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) - ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) -} - -// DeploymentClientImpl is an implementation of DeploymentClient. -type DeploymentClientImpl struct { - connection sdk.Connection -} - -// NewDeploymentClient creates a new DeploymentClient -// with the given connection. -func NewDeploymentClient(connection sdk.Connection) *DeploymentClientImpl { - return &DeploymentClientImpl{connection: connection} -} - -var _ DeploymentClient = (*DeploymentClientImpl)(nil) - -// CreateOrUpdate creates or updates a deployment. -func (rdc *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { - return rdc.inner.CreateOrUpdate(ctx, parameters, resourceID, apiVersion) -} - -// ContinueCreateOperation continues a create operation. -func (rdc *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { - return rdc.inner.ContinueCreateOperation(ctx, resumeToken) -} - -// Delete deletes a deployment. -func (rdc *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) { - return rdc.inner.Delete(ctx, resourceID, apiVersion) -} - -// ContinueDeleteOperation continues a delete operation. -func (rdc *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) { - return rdc.inner.ContinueDeleteOperation(ctx, resumeToken) -} - -var _ ResourceDeploymentsClient = (*ResourceDeploymentsClientImpl)(nil) - -// ResourceDeploymentsClientImpl is an implementation of ResourceDeploymentsClient. -type ResourceDeploymentsClientImpl struct { - inner sdkclients.ResourceDeploymentsClient -} - -// ResourceDeployments returns a ResourceDeploymentsClient. -func (c *DeploymentClientImpl) ResourceDeployments() ResourceDeploymentsClient { - rdc, err := sdkclients.NewResourceDeploymentsClient(&sdkclients.Options{ - Cred: &aztoken.AnonymousCredential{}, - BaseURI: c.connection.Endpoint(), - ARMClientOptions: sdk.NewClientOptions(c.connection), - }) - if err != nil { - panic(fmt.Errorf("failed to create client: %w", err)) - } - - return &ResourceDeploymentsClientImpl{inner: rdc} -} diff --git a/pkg/controller/reconciler/deployment_reconciler.go b/pkg/controller/reconciler/deployment_reconciler.go index 9079570c3c..5d01a7864d 100644 --- a/pkg/controller/reconciler/deployment_reconciler.go +++ b/pkg/controller/reconciler/deployment_reconciler.go @@ -43,6 +43,7 @@ import ( radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/kubernetes" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/radius-project/radius/pkg/ucp/ucplog" @@ -403,7 +404,7 @@ func (r *DeploymentReconciler) reconcileDelete(ctx context.Context, deployment * return ctrl.Result{}, nil } -func (r *DeploymentReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, deployment *appsv1.Deployment, annotations *deploymentAnnotations) (Poller[v20231001preview.ContainersClientCreateOrUpdateResponse], Poller[v20231001preview.ContainersClientDeleteResponse], bool, error) { +func (r *DeploymentReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, deployment *appsv1.Deployment, annotations *deploymentAnnotations) (sdkclients.Poller[v20231001preview.ContainersClientCreateOrUpdateResponse], sdkclients.Poller[v20231001preview.ContainersClientDeleteResponse], bool, error) { logger := ucplog.FromContextOrDiscard(ctx) resourceID := annotations.Status.Scope + "/providers/Applications.Core/containers/" + deployment.Name @@ -479,7 +480,7 @@ func (r *DeploymentReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Con return poller, nil, false, nil } -func (r *DeploymentReconciler) startDeleteOperationIfNeeded(ctx context.Context, annotations *deploymentAnnotations) (Poller[v20231001preview.ContainersClientDeleteResponse], error) { +func (r *DeploymentReconciler) startDeleteOperationIfNeeded(ctx context.Context, annotations *deploymentAnnotations) (sdkclients.Poller[v20231001preview.ContainersClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) if annotations.Status.Container == "" { logger.Info("Container is already deleted (or was never created).") diff --git a/pkg/controller/reconciler/deploymentresource_reconciler.go b/pkg/controller/reconciler/deploymentresource_reconciler.go index a3bdebca0e..1c63d49ead 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler.go @@ -53,8 +53,8 @@ type DeploymentResourceReconciler struct { // Radius is the Radius client. Radius RadiusClient - // DeploymentClient is the UCP Deployments client. - DeploymentClient DeploymentClient + // ResourceDeploymentsClient is the client for managing deployments. + ResourceDeploymentsClient sdkclients.ResourceDeploymentsClient // DelayInterval is the amount of time to wait between operations. DelayInterval time.Duration @@ -130,7 +130,7 @@ func (r *DeploymentResourceReconciler) reconcileOperation(ctx context.Context, d if deploymentResource.Status.Operation.OperationKind == radappiov1alpha3.OperationKindDelete { - poller, err := r.DeploymentClient.ResourceDeployments().ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) + poller, err := r.ResourceDeploymentsClient.ContinueDeleteOperation(ctx, deploymentResource.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue DELETE operation: %w", err) } @@ -292,14 +292,14 @@ func (r *DeploymentResourceReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } -func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (Poller[sdkclients.ClientDeleteResponse], error) { +func (r *DeploymentResourceReconciler) startDeleteOperation(ctx context.Context, deploymentResource *radappiov1alpha3.DeploymentResource) (sdkclients.Poller[sdkclients.ClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) resourceId := deploymentResource.Spec.Id radiusAPIVersion := "2023-10-01-preview" logger.Info("Starting DELETE operation.") - poller, err := r.DeploymentClient.ResourceDeployments().Delete(ctx, resourceId, radiusAPIVersion) + poller, err := r.ResourceDeploymentsClient.Delete(ctx, resourceId, radiusAPIVersion) if err != nil { return nil, err } else if poller != nil { diff --git a/pkg/controller/reconciler/deploymentresource_reconciler_test.go b/pkg/controller/reconciler/deploymentresource_reconciler_test.go index bedfc83c04..3131a76b19 100644 --- a/pkg/controller/reconciler/deploymentresource_reconciler_test.go +++ b/pkg/controller/reconciler/deploymentresource_reconciler_test.go @@ -22,6 +22,7 @@ import ( "time" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/assert" @@ -50,7 +51,7 @@ var ( TestDeploymentResourceID = fmt.Sprintf("%s/providers/Microsoft.Resources/deployments/%s", TestDeploymentResourceScope, TestDeploymentResourceName) ) -func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, *mockDeploymentClient, k8sClient.Client) { +func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, *sdkclients.MockResourceDeploymentsClient, k8sClient.Client) { SkipWithoutEnvironment(t) // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail @@ -78,15 +79,15 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, *mockDeployme require.NoError(t, err) mockRadiusClient := NewMockRadiusClient() - mockDeploymentClient := NewMockDeploymentClient() + mockResourceDeploymentsClient := sdkclients.NewMockResourceDeploymentsClient() err = (&DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: mockRadiusClient, - DeploymentClient: mockDeploymentClient, - DelayInterval: DeploymentResourceTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: mockRadiusClient, + ResourceDeploymentsClient: mockResourceDeploymentsClient, + DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) @@ -95,7 +96,7 @@ func SetupDeploymentResourceTest(t *testing.T) (*mockRadiusClient, *mockDeployme require.NoError(t, err) }() - return mockRadiusClient, mockDeploymentClient, mgr.GetClient() + return mockRadiusClient, mockResourceDeploymentsClient, mgr.GetClient() } func Test_DeploymentResourceReconciler_Basic(t *testing.T) { diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler.go b/pkg/controller/reconciler/deploymenttemplate_reconciler.go index 35151228ce..b0c04e7cc9 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler.go @@ -55,8 +55,8 @@ type DeploymentTemplateReconciler struct { // Radius is the Radius client. Radius RadiusClient - // DeploymentClient is the UCP Deployments client. - DeploymentClient DeploymentClient + // ResourceDeploymentsClient is the client for managing deployments. + ResourceDeploymentsClient sdkclients.ResourceDeploymentsClient // DelayInterval is the amount of time to wait between operations. DelayInterval time.Duration @@ -128,7 +128,7 @@ func (r *DeploymentTemplateReconciler) reconcileOperation(ctx context.Context, d logger := ucplog.FromContextOrDiscard(ctx) if deploymentTemplate.Status.Operation.OperationKind == radappiov1alpha3.OperationKindPut { - poller, err := r.DeploymentClient.ResourceDeployments().ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) + poller, err := r.ResourceDeploymentsClient.ContinueCreateOperation(ctx, deploymentTemplate.Status.Operation.ResumeToken) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to continue PUT operation: %w", err) } @@ -411,7 +411,7 @@ func (r *DeploymentTemplateReconciler) reconcileDelete(ctx context.Context, depl return ctrl.Result{Requeue: true, RequeueAfter: r.requeueDelay()}, nil } -func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { +func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Context, deploymentTemplate *radappiov1alpha3.DeploymentTemplate) (sdkclients.Poller[sdkclients.ClientCreateOrUpdateResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) specParameters := convertToARMJSONParameters(deploymentTemplate.Spec.Parameters) @@ -454,7 +454,7 @@ func (r *DeploymentTemplateReconciler) startPutOperationIfNeeded(ctx context.Con resourceID := providerConfig.Deployments.Value.Scope + "/providers/" + "Microsoft.Resources/deployments" + "/" + deploymentName logger.Info("Starting PUT operation.") - poller, err := r.DeploymentClient.ResourceDeployments().CreateOrUpdate(ctx, + poller, err := r.ResourceDeploymentsClient.CreateOrUpdate(ctx, sdkclients.Deployment{ Properties: &sdkclients.DeploymentProperties{ Template: template, diff --git a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go index b84493d1a1..bc47bc6956 100644 --- a/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go +++ b/pkg/controller/reconciler/deploymenttemplate_reconciler_test.go @@ -46,7 +46,7 @@ const ( deploymentTemplateTestControllerDelayInterval = time.Millisecond * 100 ) -func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, *mockDeploymentClient, k8sclient.Client) { +func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, *sdkclients.MockResourceDeploymentsClient, k8sclient.Client) { SkipWithoutEnvironment(t) // For debugging, you can set uncomment this to see logs from the controller. This will cause tests to fail @@ -74,27 +74,27 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, *mockDeployme require.NoError(t, err) mockRadiusClient := NewMockRadiusClient() - mockDeploymentClient := NewMockDeploymentClient() + mockResourceDeploymentsClient := sdkclients.NewMockResourceDeploymentsClient() // Set up DeploymentTemplateReconciler. err = (&DeploymentTemplateReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), - Radius: mockRadiusClient, - DeploymentClient: mockDeploymentClient, - DelayInterval: deploymentTemplateTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: mockRadiusClient, + ResourceDeploymentsClient: mockResourceDeploymentsClient, + DelayInterval: deploymentTemplateTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) // Set up DeploymentResourceReconciler. err = (&DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: mockRadiusClient, - DeploymentClient: mockDeploymentClient, - DelayInterval: DeploymentResourceTestControllerDelayInterval, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: mockRadiusClient, + ResourceDeploymentsClient: mockResourceDeploymentsClient, + DelayInterval: DeploymentResourceTestControllerDelayInterval, }).SetupWithManager(mgr) require.NoError(t, err) @@ -103,7 +103,7 @@ func SetupDeploymentTemplateTest(t *testing.T) (*mockRadiusClient, *mockDeployme require.NoError(t, err) }() - return mockRadiusClient, mockDeploymentClient, mgr.GetClient() + return mockRadiusClient, mockResourceDeploymentsClient, mgr.GetClient() } func Test_DeploymentTemplateReconciler_ComputeHash(t *testing.T) { @@ -350,14 +350,14 @@ func Test_DeploymentTemplateReconciler_FailureRecovery(t *testing.T) { // Complete the operation, but make it fail. operation := status.Operation - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - state.err = errors.New("failure") + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + state.Err = errors.New("failure") - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.ProvisioningState = to.Ptr(armresources.ProvisioningStateFailed) - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should (eventually) start a new provisioning operation @@ -416,14 +416,14 @@ func Test_DeploymentTemplateReconciler_WithResources(t *testing.T) { status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation. - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-withresources-env")}, } - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should be ready after the operation completes. @@ -493,14 +493,14 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation. - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env")}, } - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should be ready after the operation completes. @@ -542,14 +542,14 @@ func Test_DeploymentTemplateReconciler_Update(t *testing.T) { status = waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation again. - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-update/providers/Applications.Core/environments/deploymenttemplate-update-env")}, } - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should be Ready again after the operation completes. @@ -622,8 +622,8 @@ func Test_DeploymentTemplateReconciler_OutputResources(t *testing.T) { status := waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation. - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ @@ -631,7 +631,7 @@ func Test_DeploymentTemplateReconciler_OutputResources(t *testing.T) { {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application")}, {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/containers/deploymenttemplate-outputresources-container")}, } - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // DeploymentTemplate should be ready after the operation completes. @@ -692,15 +692,15 @@ func Test_DeploymentTemplateReconciler_OutputResources(t *testing.T) { status = waitForDeploymentTemplateStateUpdating(t, k8sClient, namespacedName, nil) // Complete the operation again, with a different set of output resources. - mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := mockDeploymentClient.resourceDeployments[state.resourceID] + mockDeploymentClient.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := mockDeploymentClient.GetResource(state.ResourceID) require.True(t, ok, "failed to find resource") resource.Properties.OutputResources = []*armresources.ResourceReference{ {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/environments/deploymenttemplate-outputresources-environment")}, {ID: to.Ptr("/planes/radius/local/resourceGroups/deploymenttemplate-outputresources/providers/Applications.Core/applications/deploymenttemplate-outputresources-application")}, } - state.value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} + state.Value = sdkclients.ClientCreateOrUpdateResponse{DeploymentExtended: armresources.DeploymentExtended{Properties: resource.Properties}} }) // Complete the delete operation on the container resource. diff --git a/pkg/controller/reconciler/mock_deployments_client_test.go b/pkg/controller/reconciler/mock_deployments_client_test.go deleted file mode 100644 index 0ce8f709e1..0000000000 --- a/pkg/controller/reconciler/mock_deployments_client_test.go +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright 2024 The Radius Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconciler - -import ( - "context" - "net/http" - "sync" - - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" - "github.com/google/uuid" - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" -) - -// This file contains mocks for the DeploymentClient interface. - -func NewMockDeploymentClient() *mockDeploymentClient { - return &mockDeploymentClient{ - resourceDeployments: map[string]sdkclients.ClientCreateOrUpdateResponse{}, - operations: map[string]*operationState{}, - - lock: &sync.Mutex{}, - } -} - -var _ DeploymentClient = (*mockDeploymentClient)(nil) - -type mockDeploymentClient struct { - resourceDeployments map[string]sdkclients.ClientCreateOrUpdateResponse - operations map[string]*operationState - - lock *sync.Mutex -} - -func (dc *mockDeploymentClient) ResourceDeployments() ResourceDeploymentsClient { - return &mockResourceDeploymentsClient{mock: dc} -} - -func (dc *mockDeploymentClient) CompleteOperation(operationID string, update func(state *operationState)) { - dc.lock.Lock() - defer dc.lock.Unlock() - - state, ok := dc.operations[operationID] - if !ok { - panic("operation not found: " + operationID) - } - - if update != nil { - update(state) - } - - state.complete = true -} - -var _ ResourceDeploymentsClient = (*mockResourceDeploymentsClient)(nil) - -type mockResourceDeploymentsClient struct { - mock *mockDeploymentClient -} - -func (rdc *mockResourceDeploymentsClient) CreateOrUpdate(ctx context.Context, parameters sdkclients.Deployment, resourceID, apiVersion string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { - rdc.mock.lock.Lock() - defer rdc.mock.lock.Unlock() - - value := sdkclients.ClientCreateOrUpdateResponse{ - DeploymentExtended: armresources.DeploymentExtended{ - ID: &resourceID, - Properties: &armresources.DeploymentPropertiesExtended{}, - }, - } - state := &operationState{ - kind: http.MethodPut, - resourceID: resourceID, - value: value, - } - - operationID := uuid.New().String() - rdc.mock.resourceDeployments[resourceID] = value - rdc.mock.operations[operationID] = state - - return &mockDeploymentClientPoller[sdkclients.ClientCreateOrUpdateResponse]{ - mock: rdc.mock, - operationID: operationID, - state: state, - }, nil -} - -func (rdc *mockResourceDeploymentsClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientCreateOrUpdateResponse], error) { - rdc.mock.lock.Lock() - defer rdc.mock.lock.Unlock() - - state, ok := rdc.mock.operations[resumeToken] - if !ok { - return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} - } - - return &mockDeploymentClientPoller[sdkclients.ClientCreateOrUpdateResponse]{ - operationID: resumeToken, - mock: rdc.mock, - state: state, - }, nil -} - -func (rdc *mockResourceDeploymentsClient) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[sdkclients.ClientDeleteResponse], error) { - rdc.mock.lock.Lock() - defer rdc.mock.lock.Unlock() - - state := &operationState{ - kind: http.MethodDelete, - resourceID: resourceID, - value: sdkclients.ClientDeleteResponse{ - DeploymentExtended: armresources.DeploymentExtended{ - ID: &resourceID, - Properties: &armresources.DeploymentPropertiesExtended{}, - }, - }, - } - - operationID := uuid.New().String() - rdc.mock.operations[operationID] = state - - return &mockDeploymentClientPoller[sdkclients.ClientDeleteResponse]{ - mock: rdc.mock, - operationID: operationID, - state: state, - }, nil -} - -func (rdc *mockResourceDeploymentsClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[sdkclients.ClientDeleteResponse], error) { - rdc.mock.lock.Lock() - defer rdc.mock.lock.Unlock() - - state, ok := rdc.mock.operations[resumeToken] - if !ok { - return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} - } - - return &mockDeploymentClientPoller[sdkclients.ClientDeleteResponse]{ - operationID: resumeToken, - mock: rdc.mock, - state: state, - }, nil -} - -var _ Poller[sdkclients.ClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[sdkclients.ClientCreateOrUpdateResponse])(nil) - -type mockDeploymentClientPoller[T any] struct { - operationID string - mock *mockDeploymentClient - state *operationState -} - -func (mp *mockDeploymentClientPoller[T]) Done() bool { - mp.mock.lock.Lock() - defer mp.mock.lock.Unlock() - - return mp.state.complete // Status updates are delivered via the Poll function. -} - -func (mp *mockDeploymentClientPoller[T]) Poll(ctx context.Context) (*http.Response, error) { - mp.mock.lock.Lock() - defer mp.mock.lock.Unlock() - - // NOTE: this is ok because our code ignores the actual result. - mp.state = mp.mock.operations[mp.operationID] - return nil, nil -} - -func (mp *mockDeploymentClientPoller[T]) Result(ctx context.Context) (T, error) { - mp.mock.lock.Lock() - defer mp.mock.lock.Unlock() - - if mp.state.complete && mp.state.err != nil { - return mp.state.value.(T), mp.state.err - } else if mp.state.complete { - return mp.state.value.(T), nil - } - - panic("operation not done") -} - -func (mp *mockDeploymentClientPoller[T]) ResumeToken() (string, error) { - return mp.operationID, nil -} diff --git a/pkg/controller/reconciler/mock_radius_client_test.go b/pkg/controller/reconciler/mock_radius_client_test.go index 76c60f769b..de69cacea1 100644 --- a/pkg/controller/reconciler/mock_radius_client_test.go +++ b/pkg/controller/reconciler/mock_radius_client_test.go @@ -28,6 +28,7 @@ import ( v1 "github.com/radius-project/radius/pkg/armrpc/api/v1" "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" ucpv20231001preview "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" ) @@ -41,7 +42,7 @@ func NewMockRadiusClient() *mockRadiusClient { environments: map[string]corerpv20231001preview.EnvironmentResource{}, groups: map[string]ucpv20231001preview.ResourceGroupResource{}, resources: map[string]generated.GenericResource{}, - operations: map[string]*operationState{}, + operations: map[string]*sdkclients.OperationState{}, lock: &sync.Mutex{}, } @@ -55,20 +56,11 @@ type mockRadiusClient struct { environments map[string]corerpv20231001preview.EnvironmentResource groups map[string]ucpv20231001preview.ResourceGroupResource resources map[string]generated.GenericResource - operations map[string]*operationState + operations map[string]*sdkclients.OperationState lock *sync.Mutex } -type operationState struct { - complete bool - value any - // Ideally we'd use azcore.ResponseError here, but it's tricky to set up in tests. - err error - resourceID string - kind string -} - func (rc *mockRadiusClient) Update(exec func()) { rc.lock.Lock() defer rc.lock.Unlock() @@ -103,7 +95,7 @@ func (rc *mockRadiusClient) Resources(scope string, resourceType string) Resourc return &mockResourceClient{mock: rc, scope: scope, resourceType: resourceType} } -func (rc *mockRadiusClient) CompleteOperation(operationID string, update func(state *operationState)) { +func (rc *mockRadiusClient) CompleteOperation(operationID string, update func(state *sdkclients.OperationState)) { rc.lock.Lock() defer rc.lock.Unlock() @@ -116,14 +108,14 @@ func (rc *mockRadiusClient) CompleteOperation(operationID string, update func(st update(state) } - state.complete = true + state.Complete = true - if state.kind == http.MethodDelete { - delete(rc.environments, state.resourceID) - delete(rc.applications, state.resourceID) - delete(rc.containers, state.resourceID) - delete(rc.groups, state.resourceID) - delete(rc.resources, state.resourceID) + if state.Kind == http.MethodDelete { + delete(rc.environments, state.ResourceID) + delete(rc.applications, state.ResourceID) + delete(rc.containers, state.ResourceID) + delete(rc.groups, state.ResourceID) + delete(rc.resources, state.ResourceID) } } @@ -184,14 +176,14 @@ func (cc *mockContainerClient) id(containerName string) string { return cc.scope + "/providers/Applications.Core/containers/" + containerName } -func (cc *mockContainerClient) BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { +func (cc *mockContainerClient) BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { id := cc.id(containerName) cc.mock.lock.Lock() defer cc.mock.lock.Unlock() value := corerpv20231001preview.ContainersClientCreateOrUpdateResponse{ContainerResource: resource} - state := &operationState{kind: http.MethodPut, value: value, resourceID: id} + state := &sdkclients.OperationState{Kind: http.MethodPut, Value: value, ResourceID: id} operationID := uuid.New().String() cc.mock.containers[id] = resource @@ -200,14 +192,14 @@ func (cc *mockContainerClient) BeginCreateOrUpdate(ctx context.Context, containe return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: operationID, state: state}, nil } -func (cc *mockContainerClient) BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { +func (cc *mockContainerClient) BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { id := cc.id(containerName) cc.mock.lock.Lock() defer cc.mock.lock.Unlock() value := corerpv20231001preview.ContainersClientDeleteResponse{} - state := &operationState{kind: http.MethodDelete, value: value, resourceID: id} + state := &sdkclients.OperationState{Kind: http.MethodDelete, Value: value, ResourceID: id} operationID := uuid.New().String() cc.mock.operations[operationID] = state @@ -215,7 +207,7 @@ func (cc *mockContainerClient) BeginDelete(ctx context.Context, containerName st return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientDeleteResponse]{mock: cc.mock, operationID: operationID, state: state}, nil } -func (cc *mockContainerClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { +func (cc *mockContainerClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { cc.mock.lock.Lock() defer cc.mock.lock.Unlock() @@ -227,7 +219,7 @@ func (cc *mockContainerClient) ContinueCreateOperation(ctx context.Context, resu return &mockRadiusClientPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse]{mock: cc.mock, operationID: resumeToken, state: state}, nil } -func (cc *mockContainerClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { +func (cc *mockContainerClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { cc.mock.lock.Lock() defer cc.mock.lock.Unlock() @@ -321,14 +313,14 @@ func (rc *mockResourceClient) id(resourceName string) string { return rc.scope + "/providers/" + rc.resourceType + "/" + resourceName } -func (rc *mockResourceClient) BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func (rc *mockResourceClient) BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { id := rc.id(resourceName) rc.mock.lock.Lock() defer rc.mock.lock.Unlock() value := generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} - state := &operationState{kind: http.MethodPut, value: value, resourceID: id} + state := &sdkclients.OperationState{Kind: http.MethodPut, Value: value, ResourceID: id} operationID := uuid.New().String() rc.mock.resources[id] = resource @@ -337,14 +329,14 @@ func (rc *mockResourceClient) BeginCreateOrUpdate(ctx context.Context, resourceN return &mockRadiusClientPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: operationID, state: state}, nil } -func (rc *mockResourceClient) BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (rc *mockResourceClient) BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { id := rc.id(resourceName) rc.mock.lock.Lock() defer rc.mock.lock.Unlock() value := generated.GenericResourcesClientDeleteResponse{} - state := &operationState{kind: http.MethodDelete, value: value, resourceID: id} + state := &sdkclients.OperationState{Kind: http.MethodDelete, Value: value, ResourceID: id} operationID := uuid.New().String() rc.mock.operations[operationID] = state @@ -352,7 +344,7 @@ func (rc *mockResourceClient) BeginDelete(ctx context.Context, resourceName stri return &mockRadiusClientPoller[generated.GenericResourcesClientDeleteResponse]{mock: rc.mock, operationID: operationID, state: state}, nil } -func (rc *mockResourceClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func (rc *mockResourceClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { rc.mock.lock.Lock() defer rc.mock.lock.Unlock() @@ -364,7 +356,7 @@ func (rc *mockResourceClient) ContinueCreateOperation(ctx context.Context, resum return &mockRadiusClientPoller[generated.GenericResourcesClientCreateOrUpdateResponse]{mock: rc.mock, operationID: resumeToken, state: state}, nil } -func (rc *mockResourceClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (rc *mockResourceClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { rc.mock.lock.Lock() defer rc.mock.lock.Unlock() @@ -418,19 +410,19 @@ func (rc *mockResourceClient) ListSecrets(ctx context.Context, resourceName stri return generated.GenericResourcesClientListSecretsResponse{Value: secrets}, nil } -var _ Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse])(nil) +var _ sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse] = (*mockRadiusClientPoller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse])(nil) type mockRadiusClientPoller[T any] struct { operationID string mock *mockRadiusClient - state *operationState + state *sdkclients.OperationState } func (mp *mockRadiusClientPoller[T]) Done() bool { mp.mock.lock.Lock() defer mp.mock.lock.Unlock() - return mp.state.complete // Status updates are delivered via the Poll function. + return mp.state.Complete // Status updates are delivered via the Poll function. } func (mp *mockRadiusClientPoller[T]) Poll(ctx context.Context) (*http.Response, error) { @@ -446,10 +438,10 @@ func (mp *mockRadiusClientPoller[T]) Result(ctx context.Context) (T, error) { mp.mock.lock.Lock() defer mp.mock.lock.Unlock() - if mp.state.complete && mp.state.err != nil { - return mp.state.value.(T), mp.state.err - } else if mp.state.complete { - return mp.state.value.(T), nil + if mp.state.Complete && mp.state.Err != nil { + return mp.state.Value.(T), mp.state.Err + } else if mp.state.Complete { + return mp.state.Value.(T), nil } panic("operation not done") @@ -458,3 +450,7 @@ func (mp *mockRadiusClientPoller[T]) Result(ctx context.Context) (T, error) { func (mp *mockRadiusClientPoller[T]) ResumeToken() (string, error) { return mp.operationID, nil } + +func (mp *mockRadiusClientPoller[T]) PollUntilDone(ctx context.Context, options *azcoreruntime.PollUntilDoneOptions) (T, error) { + return mp.Result(ctx) +} diff --git a/pkg/controller/reconciler/radius_client.go b/pkg/controller/reconciler/radius_client.go index 744756d348..dae6108b9c 100644 --- a/pkg/controller/reconciler/radius_client.go +++ b/pkg/controller/reconciler/radius_client.go @@ -23,6 +23,7 @@ import ( "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" "github.com/radius-project/radius/pkg/sdk" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" ucpv20231001preview "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" "github.com/radius-project/radius/pkg/ucp/resources" ) @@ -42,10 +43,10 @@ type ApplicationClient interface { } type ContainerClient interface { - BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) - BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) - ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) - ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) + BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) + BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) + ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) + ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) Get(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientGetOptions) (corerpv20231001preview.ContainersClientGetResponse, error) } @@ -59,10 +60,10 @@ type ResourceGroupClient interface { } type ResourceClient interface { - BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) - BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (Poller[generated.GenericResourcesClientDeleteResponse], error) - ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) - ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientDeleteResponse], error) + BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) + BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) + ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) + ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) Get(ctx context.Context, resourceName string) (generated.GenericResourcesClientGetResponse, error) ListSecrets(ctx context.Context, resourceName string) (generated.GenericResourcesClientListSecretsResponse, error) } @@ -146,19 +147,19 @@ type ContainerClientImpl struct { inner *corerpv20231001preview.ContainersClient } -func (cc *ContainerClientImpl) BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { +func (cc *ContainerClientImpl) BeginCreateOrUpdate(ctx context.Context, containerName string, resource corerpv20231001preview.ContainerResource, options *corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { return cc.inner.BeginCreateOrUpdate(ctx, containerName, resource, options) } -func (cc *ContainerClientImpl) BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { +func (cc *ContainerClientImpl) BeginDelete(ctx context.Context, containerName string, options *corerpv20231001preview.ContainersClientBeginDeleteOptions) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { return cc.inner.BeginDelete(ctx, containerName, options) } -func (cc *ContainerClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { +func (cc *ContainerClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { return cc.inner.BeginCreateOrUpdate(ctx, "", corerpv20231001preview.ContainerResource{}, &corerpv20231001preview.ContainersClientBeginCreateOrUpdateOptions{ResumeToken: resumeToken}) } -func (cc *ContainerClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { +func (cc *ContainerClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { return cc.inner.BeginDelete(ctx, "", &corerpv20231001preview.ContainersClientBeginDeleteOptions{ResumeToken: resumeToken}) } @@ -216,19 +217,19 @@ type ResourceClientImpl struct { inner *generated.GenericResourcesClient } -func (rc *ResourceClientImpl) BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func (rc *ResourceClientImpl) BeginCreateOrUpdate(ctx context.Context, resourceName string, resource generated.GenericResource, options *generated.GenericResourcesClientBeginCreateOrUpdateOptions) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { return rc.inner.BeginCreateOrUpdate(ctx, resourceName, resource, options) } -func (rc *ResourceClientImpl) BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (rc *ResourceClientImpl) BeginDelete(ctx context.Context, resourceName string, options *generated.GenericResourcesClientBeginDeleteOptions) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { return rc.inner.BeginDelete(ctx, resourceName, options) } -func (rc *ResourceClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func (rc *ResourceClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { return rc.inner.BeginCreateOrUpdate(ctx, "", generated.GenericResource{}, &generated.GenericResourcesClientBeginCreateOrUpdateOptions{ResumeToken: resumeToken}) } -func (rc *ResourceClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (rc *ResourceClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { return rc.inner.BeginDelete(ctx, "", &generated.GenericResourcesClientBeginDeleteOptions{ResumeToken: resumeToken}) } diff --git a/pkg/controller/reconciler/recipe_reconciler.go b/pkg/controller/reconciler/recipe_reconciler.go index 527363d530..7e0803515f 100644 --- a/pkg/controller/reconciler/recipe_reconciler.go +++ b/pkg/controller/reconciler/recipe_reconciler.go @@ -33,6 +33,7 @@ import ( "github.com/radius-project/radius/pkg/cli/clients" "github.com/radius-project/radius/pkg/cli/clients_new/generated" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -367,7 +368,7 @@ func (r *RecipeReconciler) reconcileDelete(ctx context.Context, recipe *radappio return ctrl.Result{}, nil } -func (r *RecipeReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, recipe *radappiov1alpha3.Recipe) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (r *RecipeReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context, recipe *radappiov1alpha3.Recipe) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) resourceID := recipe.Status.Scope + "/providers/" + recipe.Spec.Type + "/" + recipe.Name @@ -413,7 +414,7 @@ func (r *RecipeReconciler) startPutOrDeleteOperationIfNeeded(ctx context.Context return nil, nil, nil } -func (r *RecipeReconciler) startDeleteOperationIfNeeded(ctx context.Context, recipe *radappiov1alpha3.Recipe) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func (r *RecipeReconciler) startDeleteOperationIfNeeded(ctx context.Context, recipe *radappiov1alpha3.Recipe) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { logger := ucplog.FromContextOrDiscard(ctx) if recipe.Status.Resource == "" { logger.Info("Resource is already deleted (or was never created).") diff --git a/pkg/controller/reconciler/recipe_reconciler_test.go b/pkg/controller/reconciler/recipe_reconciler_test.go index d8a3dadff4..04ff914046 100644 --- a/pkg/controller/reconciler/recipe_reconciler_test.go +++ b/pkg/controller/reconciler/recipe_reconciler_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/radius-project/radius/pkg/cli/clients_new/generated" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" "github.com/radius-project/radius/test/testcontext" "github.com/stretchr/testify/require" @@ -217,14 +218,14 @@ func Test_RecipeReconciler_FailureRecovery(t *testing.T) { // Complete the operation, but make it fail. operation := status.Operation - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - state.err = errors.New("oops") + radius.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + state.Err = errors.New("oops") - resource, ok := radius.resources[state.resourceID] + resource, ok := radius.resources[state.ResourceID] require.True(t, ok, "failed to find resource") resource.Properties["provisioningState"] = "Failed" - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + state.Value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // Recipe should (eventually) start a new provisioning operation @@ -242,10 +243,10 @@ func Test_RecipeReconciler_FailureRecovery(t *testing.T) { // Complete the operation, but make it fail. operation = status.Operation - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - state.err = errors.New("oops") + radius.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + state.Err = errors.New("oops") - resource, ok := radius.resources[state.resourceID] + resource, ok := radius.resources[state.ResourceID] require.True(t, ok, "failed to find resource") resource.Properties["provisioningState"] = "Failed" @@ -282,15 +283,15 @@ func Test_RecipeReconciler_WithSecret(t *testing.T) { status := waitForRecipeStateUpdating(t, client, name, nil) // Update the resource with computed values as part of completing the operation. - radius.CompleteOperation(status.Operation.ResumeToken, func(state *operationState) { - resource, ok := radius.resources[state.resourceID] + radius.CompleteOperation(status.Operation.ResumeToken, func(state *sdkclients.OperationState) { + resource, ok := radius.resources[state.ResourceID] require.True(t, ok, "failed to find resource") resource.Properties["a-value"] = "a" resource.Properties["secrets"] = map[string]string{ "b-secret": "b", } - state.value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} + state.Value = generated.GenericResourcesClientCreateOrUpdateResponse{GenericResource: resource} }) // Recipe will update after operation completes diff --git a/pkg/controller/reconciler/util.go b/pkg/controller/reconciler/util.go index 91c027d1e1..f85254f774 100644 --- a/pkg/controller/reconciler/util.go +++ b/pkg/controller/reconciler/util.go @@ -25,6 +25,7 @@ import ( "github.com/radius-project/radius/pkg/cli/clients" "github.com/radius-project/radius/pkg/cli/clients_new/generated" corerpv20231001preview "github.com/radius-project/radius/pkg/corerp/api/v20231001preview" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/to" ucpv20231001preview "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" "github.com/radius-project/radius/pkg/ucp/resources" @@ -151,7 +152,7 @@ func createApplicationIfNotExists(ctx context.Context, radius RadiusClient, envi return nil } -func deleteResource(ctx context.Context, radius RadiusClient, resourceID string) (Poller[generated.GenericResourcesClientDeleteResponse], error) { +func deleteResource(ctx context.Context, radius RadiusClient, resourceID string) (sdkclients.Poller[generated.GenericResourcesClientDeleteResponse], error) { id, err := resources.Parse(resourceID) if err != nil { return nil, err @@ -178,7 +179,7 @@ func deleteResource(ctx context.Context, radius RadiusClient, resourceID string) return nil, nil } -func createOrUpdateResource(ctx context.Context, radius RadiusClient, resourceID string, properties map[string]any) (Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { +func createOrUpdateResource(ctx context.Context, radius RadiusClient, resourceID string, properties map[string]any) (sdkclients.Poller[generated.GenericResourcesClientCreateOrUpdateResponse], error) { id, err := resources.Parse(resourceID) if err != nil { return nil, err @@ -222,7 +223,7 @@ func fetchResource(ctx context.Context, radius RadiusClient, resourceID string) return radius.Resources(id.RootScope(), id.Type()).Get(ctx, id.Name()) } -func deleteContainer(ctx context.Context, radius RadiusClient, containerID string) (Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { +func deleteContainer(ctx context.Context, radius RadiusClient, containerID string) (sdkclients.Poller[corerpv20231001preview.ContainersClientDeleteResponse], error) { id, err := resources.Parse(containerID) if err != nil { return nil, err @@ -249,7 +250,7 @@ func deleteContainer(ctx context.Context, radius RadiusClient, containerID strin return nil, nil } -func createOrUpdateContainer(ctx context.Context, radius RadiusClient, containerID string, properties *corerpv20231001preview.ContainerProperties) (Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { +func createOrUpdateContainer(ctx context.Context, radius RadiusClient, containerID string, properties *corerpv20231001preview.ContainerProperties) (sdkclients.Poller[corerpv20231001preview.ContainersClientCreateOrUpdateResponse], error) { id, err := resources.Parse(containerID) if err != nil { return nil, err diff --git a/pkg/controller/service.go b/pkg/controller/service.go index 8c2094c370..62b41bad38 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -21,9 +21,12 @@ import ( "fmt" "github.com/radius-project/radius/pkg/armrpc/hostoptions" + aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" "github.com/radius-project/radius/pkg/components/hosting" radappiov1alpha3 "github.com/radius-project/radius/pkg/controller/api/radapp.io/v1alpha3" "github.com/radius-project/radius/pkg/controller/reconciler" + "github.com/radius-project/radius/pkg/sdk" + sdkclients "github.com/radius-project/radius/pkg/sdk/clients" "github.com/radius-project/radius/pkg/ucp/ucplog" "k8s.io/apimachinery/pkg/runtime" @@ -107,22 +110,28 @@ func (s *Service) Run(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "Deployment", err) } + + resourceDeploymentsClient, err := sdkclients.NewResourceDeploymentsClient(&sdkclients.Options{ + Cred: &aztoken.AnonymousCredential{}, + BaseURI: s.Options.UCPConnection.Endpoint(), + ARMClientOptions: sdk.NewClientOptions(s.Options.UCPConnection), + }) err = (&reconciler.DeploymentTemplateReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), - Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), - DeploymentClient: reconciler.NewDeploymentClient(s.Options.UCPConnection), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymenttemplate-controller"), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), + ResourceDeploymentsClient: resourceDeploymentsClient, }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentTemplate", err) } err = (&reconciler.DeploymentResourceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), - Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), - DeploymentClient: reconciler.NewDeploymentClient(s.Options.UCPConnection), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: mgr.GetEventRecorderFor("deploymentresource-controller"), + Radius: reconciler.NewRadiusClient(s.Options.UCPConnection), + ResourceDeploymentsClient: resourceDeploymentsClient, }).SetupWithManager(mgr) if err != nil { return fmt.Errorf("failed to setup %s controller: %w", "DeploymentResource", err) diff --git a/pkg/recipes/driver/bicep.go b/pkg/recipes/driver/bicep.go index 3a60341bd7..bbf8310479 100644 --- a/pkg/recipes/driver/bicep.go +++ b/pkg/recipes/driver/bicep.go @@ -24,7 +24,6 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/go-logr/logr" "golang.org/x/sync/errgroup" @@ -160,7 +159,7 @@ func (d *bicepDriver) Execute(ctx context.Context, opts ExecuteOptions) (*recipe return nil, recipes.NewRecipeError(recipes.RecipeDeploymentFailed, fmt.Sprintf("failed to deploy recipe %s of type %s", opts.BaseOptions.Recipe.Name, opts.BaseOptions.Definition.ResourceType), recipes_util.ExecutionError, recipes.GetErrorDetails(err)) } - resp, err := poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: pollFrequency}) + resp, err := poller.PollUntilDone(ctx, &clients.PollUntilDoneOptions{Frequency: pollFrequency}) if err != nil { return nil, recipes.NewRecipeError(recipes.RecipeDeploymentFailed, fmt.Sprintf("failed to deploy recipe %s of type %s", opts.BaseOptions.Recipe.Name, opts.BaseOptions.Definition.ResourceType), recipes_util.ExecutionError, recipes.GetErrorDetails(err)) } diff --git a/pkg/sdk/clients/mock_resourcedeploymentsclient.go b/pkg/sdk/clients/mock_resourcedeploymentsclient.go new file mode 100644 index 0000000000..c02a8b2a0b --- /dev/null +++ b/pkg/sdk/clients/mock_resourcedeploymentsclient.go @@ -0,0 +1,198 @@ +/* +Copyright 2024 The Radius Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "net/http" + "sync" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" + "github.com/google/uuid" +) + +// This file contains mocks for the ResourceDeploymentsClient interface. + +func NewMockResourceDeploymentsClient() *MockResourceDeploymentsClient { + return &MockResourceDeploymentsClient{ + resourceDeployments: map[string]*ClientCreateOrUpdateResponse{}, + operations: map[string]*OperationState{}, + + lock: &sync.Mutex{}, + } +} + +var _ ResourceDeploymentsClient = (*MockResourceDeploymentsClient)(nil) + +type MockResourceDeploymentsClient struct { + resourceDeployments map[string]*ClientCreateOrUpdateResponse + operations map[string]*OperationState + + lock *sync.Mutex +} + +func (rdc *MockResourceDeploymentsClient) CompleteOperation(operationID string, update func(state *OperationState)) { + rdc.lock.Lock() + defer rdc.lock.Unlock() + + state, ok := rdc.operations[operationID] + if !ok { + panic("operation not found: " + operationID) + } + + if update != nil { + update(state) + } + + state.Complete = true +} + +func (rdc *MockResourceDeploymentsClient) CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (Poller[ClientCreateOrUpdateResponse], error) { + rdc.lock.Lock() + defer rdc.lock.Unlock() + + value := ClientCreateOrUpdateResponse{ + DeploymentExtended: armresources.DeploymentExtended{ + ID: &resourceID, + Properties: &armresources.DeploymentPropertiesExtended{}, + }, + } + state := &OperationState{ + Kind: http.MethodPut, + ResourceID: resourceID, + Value: value, + } + + operationID := uuid.New().String() + rdc.resourceDeployments[resourceID] = &value + rdc.operations[operationID] = state + + return &MockResourceDeploymentsClientPoller[ClientCreateOrUpdateResponse]{ + mock: rdc, + operationID: operationID, + state: state, + }, nil +} + +func (rdc *MockResourceDeploymentsClient) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[ClientCreateOrUpdateResponse], error) { + rdc.lock.Lock() + defer rdc.lock.Unlock() + + state, ok := rdc.operations[resumeToken] + if !ok { + return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} + } + + return &MockResourceDeploymentsClientPoller[ClientCreateOrUpdateResponse]{ + operationID: resumeToken, + mock: rdc, + state: state, + }, nil +} + +func (rdc *MockResourceDeploymentsClient) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[ClientDeleteResponse], error) { + rdc.lock.Lock() + defer rdc.lock.Unlock() + + state := &OperationState{ + Kind: http.MethodDelete, + ResourceID: resourceID, + Value: ClientDeleteResponse{ + DeploymentExtended: armresources.DeploymentExtended{ + ID: &resourceID, + Properties: &armresources.DeploymentPropertiesExtended{}, + }, + }, + } + + operationID := uuid.New().String() + rdc.operations[operationID] = state + + return &MockResourceDeploymentsClientPoller[ClientDeleteResponse]{ + mock: rdc, + operationID: operationID, + state: state, + }, nil +} + +func (rdc *MockResourceDeploymentsClient) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[ClientDeleteResponse], error) { + rdc.lock.Lock() + defer rdc.lock.Unlock() + + state, ok := rdc.operations[resumeToken] + if !ok { + return nil, &azcore.ResponseError{StatusCode: http.StatusNotFound} + } + + return &MockResourceDeploymentsClientPoller[ClientDeleteResponse]{ + operationID: resumeToken, + mock: rdc, + state: state, + }, nil +} + +func (rdc *MockResourceDeploymentsClient) GetResource(resourceID string) (*ClientCreateOrUpdateResponse, bool) { + resource, ok := rdc.resourceDeployments[resourceID] + + return resource, ok +} + +type MockResourceDeploymentsClientPoller[T any] struct { + operationID string + mock *MockResourceDeploymentsClient + state *OperationState +} + +var _ Poller[ClientCreateOrUpdateResponse] = (*MockResourceDeploymentsClientPoller[ClientCreateOrUpdateResponse])(nil) + +func (mp *MockResourceDeploymentsClientPoller[T]) Done() bool { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + return mp.state.Complete // Status updates are delivered via the Poll function. +} + +func (mp *MockResourceDeploymentsClientPoller[T]) Poll(ctx context.Context) (*http.Response, error) { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + // NOTE: this is ok because our code ignores the actual result. + mp.state = mp.mock.operations[mp.operationID] + return nil, nil +} + +func (mp *MockResourceDeploymentsClientPoller[T]) Result(ctx context.Context) (T, error) { + mp.mock.lock.Lock() + defer mp.mock.lock.Unlock() + + if mp.state.Complete && mp.state.Err != nil { + return mp.state.Value.(T), mp.state.Err + } else if mp.state.Complete { + return mp.state.Value.(T), nil + } + + panic("operation not done") +} + +func (mp *MockResourceDeploymentsClientPoller[T]) ResumeToken() (string, error) { + return mp.operationID, nil +} + +func (mp *MockResourceDeploymentsClientPoller[T]) PollUntilDone(ctx context.Context, options *PollUntilDoneOptions) (T, error) { + return mp.Result(ctx) +} diff --git a/pkg/controller/reconciler/poller.go b/pkg/sdk/clients/poller.go similarity index 51% rename from pkg/controller/reconciler/poller.go rename to pkg/sdk/clients/poller.go index d349200061..2f19be1a3d 100644 --- a/pkg/controller/reconciler/poller.go +++ b/pkg/sdk/clients/poller.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -14,21 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -package reconciler +package clients import ( "context" "net/http" azcoreruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - sdkclients "github.com/radius-project/radius/pkg/sdk/clients" ) +// Poller is an interface for polling an operation. +// This uses the same functions of the Poller struct from the Azure SDK for Go. +// We use this interface to allow mocking the Poller struct in tests. type Poller[T any] interface { Done() bool Poll(ctx context.Context) (*http.Response, error) + PollUntilDone(ctx context.Context, options *PollUntilDoneOptions) (T, error) Result(ctx context.Context) (T, error) ResumeToken() (string, error) } -var _ Poller[sdkclients.ClientCreateOrUpdateResponse] = (*azcoreruntime.Poller[sdkclients.ClientCreateOrUpdateResponse])(nil) +type PollUntilDoneOptions = azcoreruntime.PollUntilDoneOptions + +// OperationState represents the state of an operation. +// This is a simplified version of the real OperationState that we use in testing. +type OperationState struct { + Complete bool + Value any + // Ideally we'd use azcore.ResponseError here, but it's tricky to set up in tests. + Err error + ResourceID string + Kind string +} diff --git a/pkg/sdk/clients/resourcedeploymentsclient.go b/pkg/sdk/clients/resourcedeploymentsclient.go index 15643964cd..dea38746b3 100644 --- a/pkg/sdk/clients/resourcedeploymentsclient.go +++ b/pkg/sdk/clients/resourcedeploymentsclient.go @@ -95,10 +95,10 @@ type ProviderConfig struct { // ResourceDeploymentsClient is a deployments client for Azure Resource Manager. // It is used by both Azure and UCP clients. type ResourceDeploymentsClient interface { - CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) - ContinueCreateOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) - Delete(ctx context.Context, resourceID, apiVersion string) (*runtime.Poller[ClientDeleteResponse], error) - ContinueDeleteOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientDeleteResponse], error) + CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (Poller[ClientCreateOrUpdateResponse], error) + ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[ClientCreateOrUpdateResponse], error) + Delete(ctx context.Context, resourceID, apiVersion string) (Poller[ClientDeleteResponse], error) + ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[ClientDeleteResponse], error) } type ResourceDeploymentsClientImpl struct { @@ -139,13 +139,14 @@ type ClientCreateOrUpdateResponse struct { armresources.DeploymentExtended } +// ClientDeleteResponse contains the response from method Client.Delete. type ClientDeleteResponse struct { armresources.DeploymentExtended } // CreateOrUpdate creates a request to create or update a deployment and returns a poller to // track the progress of the operation. -func (client *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) { +func (client *ResourceDeploymentsClientImpl) CreateOrUpdate(ctx context.Context, parameters Deployment, resourceID, apiVersion string) (Poller[ClientCreateOrUpdateResponse], error) { if !strings.HasPrefix(resourceID, "/") { return nil, fmt.Errorf("error creating or updating a deployment: resourceID must start with a slash") } @@ -190,13 +191,13 @@ func (client *ResourceDeploymentsClientImpl) createOrUpdateCreateRequest(ctx con } // ContinueCreateOperation continues a create operation given a resume token. -func (client *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientCreateOrUpdateResponse], error) { +func (client *ResourceDeploymentsClientImpl) ContinueCreateOperation(ctx context.Context, resumeToken string) (Poller[ClientCreateOrUpdateResponse], error) { return runtime.NewPollerFromResumeToken[ClientCreateOrUpdateResponse](resumeToken, *client.pipeline, nil) } // Delete creates a request to delete a resource and returns a poller to // track the progress of the operation. -func (client *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (*runtime.Poller[ClientDeleteResponse], error) { +func (client *ResourceDeploymentsClientImpl) Delete(ctx context.Context, resourceID, apiVersion string) (Poller[ClientDeleteResponse], error) { if !strings.HasPrefix(resourceID, "/") { return nil, fmt.Errorf("error creating or updating a deployment: resourceID must start with a slash") } @@ -241,6 +242,6 @@ func (client *ResourceDeploymentsClientImpl) deleteCreateRequest(ctx context.Con } // ContinueCreateOperation continues a create operation given a resume token. -func (client *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (*runtime.Poller[ClientDeleteResponse], error) { +func (client *ResourceDeploymentsClientImpl) ContinueDeleteOperation(ctx context.Context, resumeToken string) (Poller[ClientDeleteResponse], error) { return runtime.NewPollerFromResumeToken[ClientDeleteResponse](resumeToken, *client.pipeline, nil) } From be4c051806eda27d2d74fc82f2b5a733077680f3 Mon Sep 17 00:00:00 2001 From: willdavsmith Date: Mon, 27 Jan 2025 17:32:44 -0800 Subject: [PATCH 65/65] PR Signed-off-by: willdavsmith --- pkg/controller/service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/controller/service.go b/pkg/controller/service.go index 62b41bad38..f6d5f5093b 100644 --- a/pkg/controller/service.go +++ b/pkg/controller/service.go @@ -116,6 +116,9 @@ func (s *Service) Run(ctx context.Context) error { BaseURI: s.Options.UCPConnection.Endpoint(), ARMClientOptions: sdk.NewClientOptions(s.Options.UCPConnection), }) + if err != nil { + return fmt.Errorf("failed to create resource deployments client: %w", err) + } err = (&reconciler.DeploymentTemplateReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),