diff --git a/config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml b/config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml index 47939530bb..5d5b6a9621 100644 --- a/config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml +++ b/config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml @@ -67,6 +67,18 @@ spec: required: - name type: object + connectionSecret: + description: LocalObjectReference is a reference to an object in the + same namespace as the referent + properties: + name: + description: |- + Name of the resource being referred to + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + required: + - name + type: object deploymentSpec: description: Configuration for the advanced (v1.5) deployment API https://www.mongodb.com/docs/atlas/reference/api/clusters/ @@ -605,6 +617,16 @@ spec: required: - name type: object + externalProjectRef: + description: ExternalProjectRef holds the Atlas project ID the user + belongs to + properties: + id: + description: ID is the Atlas project ID + type: string + required: + - id + type: object processArgs: description: ProcessArgs allows to modify Advanced Configuration Options properties: @@ -822,9 +844,16 @@ spec: - name - providerSettings type: object - required: - - projectRef type: object + x-kubernetes-validations: + - message: must define only one project reference through externalProjectRef + or projectRef + rule: (has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) + && has(self.projectRef)) + - message: must define a local connection secret when referencing an external + project + rule: (has(self.externalProjectRef) && has(self.connectionSecret)) || + !has(self.externalProjectRef) status: description: AtlasDeploymentStatus defines the observed state of AtlasDeployment. properties: diff --git a/pkg/api/v1/atlasdatabaseuser_types_test.go b/pkg/api/v1/atlasdatabaseuser_types_test.go index 8b6c7689f4..9860bf8f36 100644 --- a/pkg/api/v1/atlasdatabaseuser_types_test.go +++ b/pkg/api/v1/atlasdatabaseuser_types_test.go @@ -1,33 +1,22 @@ -package v1 +package v1 // nolint: dupl import ( "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" - "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel" ) func TestProjectReference(t *testing.T) { - validator, err := cel.VersionValidatorFromFile(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", "v1") - require.NoError(t, err) - - tests := map[string]struct { - dbUser *AtlasDatabaseUser - expectedErrors []string - }{ + tests := projectReferenceTestCase{ "no project reference is set": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{}, }, expectedErrors: []string{"spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef"}, }, "both project references are set": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ Project: &common.ResourceRefNamespaced{ Name: "my-project", @@ -43,7 +32,7 @@ func TestProjectReference(t *testing.T) { }, }, "external project references is set": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", @@ -55,7 +44,7 @@ func TestProjectReference(t *testing.T) { }, }, "kubernetes project references is set": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ Project: &common.ResourceRefNamespaced{ Name: "my-project", @@ -65,32 +54,13 @@ func TestProjectReference(t *testing.T) { }, } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - unstructuredDBUser, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.dbUser) - require.NoError(t, err) - - errs := validator(unstructuredDBUser, nil) - - require.Equal(t, len(tt.expectedErrors), len(errs)) - - for i, err := range errs { - assert.Equal(t, tt.expectedErrors[i], err.Error()) - } - }) - } + assertProjectReference(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", tests) } func TestExternalProjectReferenceConnectionSecret(t *testing.T) { - validator, err := cel.VersionValidatorFromFile(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", "v1") - require.NoError(t, err) - - tests := map[string]struct { - dbUser *AtlasDatabaseUser - expectedErrors []string - }{ + tests := projectReferenceTestCase{ "external project references is set without connection secret": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", @@ -102,7 +72,7 @@ func TestExternalProjectReferenceConnectionSecret(t *testing.T) { }, }, "external project references is set with connection secret": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ ExternalProjectRef: &ExternalProjectReference{ ID: "my-project-id", @@ -116,7 +86,7 @@ func TestExternalProjectReferenceConnectionSecret(t *testing.T) { }, }, "kubernetes project references is set without connection secret": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ Project: &common.ResourceRefNamespaced{ Name: "my-project", @@ -125,7 +95,7 @@ func TestExternalProjectReferenceConnectionSecret(t *testing.T) { }, }, "kubernetes project references is set with connection secret": { - dbUser: &AtlasDatabaseUser{ + object: &AtlasDatabaseUser{ Spec: AtlasDatabaseUserSpec{ Project: &common.ResourceRefNamespaced{ Name: "my-project", @@ -140,18 +110,5 @@ func TestExternalProjectReferenceConnectionSecret(t *testing.T) { }, } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - unstructuredDBUser, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.dbUser) - require.NoError(t, err) - - errs := validator(unstructuredDBUser, nil) - - require.Equal(t, len(tt.expectedErrors), len(errs)) - - for i, err := range errs { - assert.Equal(t, tt.expectedErrors[i], err.Error()) - } - }) - } + assertExternalProjectReferenceConnectionSecret(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdatabaseusers.yaml", tests) } diff --git a/pkg/api/v1/atlasdeployment_types.go b/pkg/api/v1/atlasdeployment_types.go index 2936d8609e..b7d455d1fe 100644 --- a/pkg/api/v1/atlasdeployment_types.go +++ b/pkg/api/v1/atlasdeployment_types.go @@ -47,9 +47,15 @@ const ( // AtlasDeploymentSpec defines the desired state of AtlasDeployment // Only one of DeploymentSpec, AdvancedDeploymentSpec and ServerlessSpec should be defined +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && !has(self.projectRef)) || (!has(self.externalProjectRef) && has(self.projectRef))",message="must define only one project reference through externalProjectRef or projectRef" +// +kubebuilder:validation:XValidation:rule="(has(self.externalProjectRef) && has(self.connectionSecret)) || !has(self.externalProjectRef)",message="must define a local connection secret when referencing an external project" type AtlasDeploymentSpec struct { + api.LocalCredentialHolder `json:",inline"` + // Project is a reference to AtlasProject resource the deployment belongs to - Project common.ResourceRefNamespaced `json:"projectRef"` + Project *common.ResourceRefNamespaced `json:"projectRef,omitempty"` + // ExternalProjectRef holds the Atlas project ID the user belongs to + ExternalProjectRef *ExternalProjectReference `json:"externalProjectRef,omitempty"` // Configuration for the advanced (v1.5) deployment API https://www.mongodb.com/docs/atlas/reference/api/clusters/ // +optional @@ -520,6 +526,10 @@ func (c *AtlasDeployment) UpdateStatus(conditions []api.Condition, options ...ap } } +func (c *AtlasDeployment) Credentials() *api.LocalObjectReference { + return c.Spec.Credentials() +} + // ************************************ Builder methods ************************************************* func NewDeployment(namespace, name, nameInAtlas string) *AtlasDeployment { @@ -619,7 +629,7 @@ func (c *AtlasDeployment) WithAtlasName(name string) *AtlasDeployment { } func (c *AtlasDeployment) WithProjectName(projectName string) *AtlasDeployment { - c.Spec.Project = common.ResourceRefNamespaced{Name: projectName} + c.Spec.Project = &common.ResourceRefNamespaced{Name: projectName} return c } diff --git a/pkg/api/v1/atlasdeployment_types_test.go b/pkg/api/v1/atlasdeployment_types_test.go new file mode 100644 index 0000000000..f6af1a953f --- /dev/null +++ b/pkg/api/v1/atlasdeployment_types_test.go @@ -0,0 +1,114 @@ +package v1 // nolint: dupl + +import ( + "testing" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" +) + +func TestDeploymentProjectReference(t *testing.T) { + tests := projectReferenceTestCase{ + "no project reference is set": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{}, + }, + expectedErrors: []string{"spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef"}, + }, + "both project references are set": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define only one project reference through externalProjectRef or projectRef", + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "kubernetes project references is set": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + } + + assertProjectReference(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml", tests) +} + +func TestDeploymentExternalProjectReferenceConnectionSecret(t *testing.T) { + tests := projectReferenceTestCase{ + "external project references is set without connection secret": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + }, + }, + expectedErrors: []string{ + "spec: Invalid value: \"object\": must define a local connection secret when referencing an external project", + }, + }, + "external project references is set with connection secret": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + ExternalProjectRef: &ExternalProjectReference{ + ID: "my-project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + "kubernetes project references is set without connection secret": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + }, + }, + }, + "kubernetes project references is set with connection secret": { + object: &AtlasDeployment{ + Spec: AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "my-dbuser-connection-secret", + }, + }, + }, + }, + }, + } + + assertExternalProjectReferenceConnectionSecret(t, "../../../config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml", tests) +} diff --git a/pkg/api/v1/project_reference_cel_test.go b/pkg/api/v1/project_reference_cel_test.go new file mode 100644 index 0000000000..a9f037f024 --- /dev/null +++ b/pkg/api/v1/project_reference_cel_test.go @@ -0,0 +1,60 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/cel" +) + +type projectReferenceTestCase map[string]struct { + object AtlasCustomResource + expectedErrors []string +} + +func assertProjectReference(t *testing.T, crdPath string, tests projectReferenceTestCase) { + t.Helper() + + validator, err := cel.VersionValidatorFromFile(t, crdPath, "v1") + require.NoError(t, err) + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.object) + require.NoError(t, err) + + errs := validator(unstructuredObject, nil) + + require.Equal(t, len(tt.expectedErrors), len(errs)) + + for i, err := range errs { + assert.Equal(t, tt.expectedErrors[i], err.Error()) + } + }) + } +} + +func assertExternalProjectReferenceConnectionSecret(t *testing.T, crdPath string, tests projectReferenceTestCase) { + t.Helper() + + validator, err := cel.VersionValidatorFromFile(t, crdPath, "v1") + require.NoError(t, err) + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tt.object) + require.NoError(t, err) + + errs := validator(unstructuredObject, nil) + + require.Equal(t, len(tt.expectedErrors), len(errs)) + + for i, err := range errs { + assert.Equal(t, tt.expectedErrors[i], err.Error()) + } + }) + } +} diff --git a/pkg/api/v1/zz_generated.deepcopy.go b/pkg/api/v1/zz_generated.deepcopy.go index 938a892577..657519c604 100644 --- a/pkg/api/v1/zz_generated.deepcopy.go +++ b/pkg/api/v1/zz_generated.deepcopy.go @@ -782,7 +782,17 @@ func (in *AtlasDeploymentList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AtlasDeploymentSpec) DeepCopyInto(out *AtlasDeploymentSpec) { *out = *in - out.Project = in.Project + in.LocalCredentialHolder.DeepCopyInto(&out.LocalCredentialHolder) + if in.Project != nil { + in, out := &in.Project, &out.Project + *out = new(common.ResourceRefNamespaced) + **out = **in + } + if in.ExternalProjectRef != nil { + in, out := &in.ExternalProjectRef, &out.ExternalProjectRef + *out = new(ExternalProjectReference) + **out = **in + } if in.DeploymentSpec != nil { in, out := &in.DeploymentSpec, &out.DeploymentSpec *out = new(AdvancedDeploymentSpec) diff --git a/pkg/controller/atlasdeployment/atlasdeployment_controller.go b/pkg/controller/atlasdeployment/atlasdeployment_controller.go index 7ce1831c10..46008a9c8a 100644 --- a/pkg/controller/atlasdeployment/atlasdeployment_controller.go +++ b/pkg/controller/atlasdeployment/atlasdeployment_controller.go @@ -39,6 +39,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" @@ -87,11 +88,11 @@ type AtlasDeploymentReconciler struct { // +kubebuilder:rbac:groups="",namespace=default,resources=events,verbs=create;patch -func (r *AtlasDeploymentReconciler) Reconcile(context context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *AtlasDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.With("atlasdeployment", req.NamespacedName) atlasDeployment := &akov2.AtlasDeployment{} - result := customresource.PrepareResource(context, r.Client, req, atlasDeployment, log) + result := customresource.PrepareResource(ctx, r.Client, req, atlasDeployment, log) if !result.IsOk() { return result.ReconcileResult(), nil } @@ -99,7 +100,7 @@ func (r *AtlasDeploymentReconciler) Reconcile(context context.Context, req ctrl. if shouldSkip := customresource.ReconciliationShouldBeSkipped(atlasDeployment); shouldSkip { log.Infow(fmt.Sprintf("-> Skipping AtlasDeployment reconciliation as annotation %s=%s", customresource.ReconciliationPolicyAnnotation, customresource.ReconciliationPolicySkip), "spec", atlasDeployment.Spec) if !atlasDeployment.GetDeletionTimestamp().IsZero() { - err := r.removeDeletionFinalizer(context, atlasDeployment) + err := r.removeDeletionFinalizer(ctx, atlasDeployment) if err != nil { result = workflow.Terminate(workflow.Internal, err.Error()) log.Errorw("failed to remove finalizer", "error", err) @@ -110,60 +111,47 @@ func (r *AtlasDeploymentReconciler) Reconcile(context context.Context, req ctrl. } conditions := akov2.InitCondition(atlasDeployment, api.FalseCondition(api.ReadyType)) - ctx := workflow.NewContext(log, conditions, context) + workflowCtx := workflow.NewContext(log, conditions, ctx) log.Infow("-> Starting AtlasDeployment reconciliation", "spec", atlasDeployment.Spec, "status", atlasDeployment.Status) defer func() { - statushandler.Update(ctx, r.Client, r.EventRecorder, atlasDeployment) + statushandler.Update(workflowCtx, r.Client, r.EventRecorder, atlasDeployment) }() - resourceVersionIsValid := customresource.ValidateResourceVersion(ctx, atlasDeployment, r.Log) + resourceVersionIsValid := customresource.ValidateResourceVersion(workflowCtx, atlasDeployment, r.Log) if !resourceVersionIsValid.IsOk() { r.Log.Debugf("deployment validation result: %v", resourceVersionIsValid) return resourceVersionIsValid.ReconcileResult(), nil } - project := &akov2.AtlasProject{} - if result = r.readProjectResource(context, atlasDeployment, project); !result.IsOk() { - ctx.SetConditionFromResult(api.DeploymentReadyType, result) - return result.ReconcileResult(), nil - } - - if err := validate.AtlasDeployment(atlasDeployment, r.AtlasProvider.IsCloudGov(), project.Spec.RegionUsageRestrictions); err != nil { - result = workflow.Terminate(workflow.Internal, err.Error()) - ctx.SetConditionFromResult(api.ValidationSucceeded, result) - return result.ReconcileResult(), nil - } - ctx.SetConditionTrue(api.ValidationSucceeded) - if !r.AtlasProvider.IsResourceSupported(atlasDeployment) { result = workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasDeployment is not supported by Atlas for government"). WithoutRetry() - ctx.SetConditionFromResult(api.DeploymentReadyType, result) + workflowCtx.SetConditionFromResult(api.DeploymentReadyType, result) return result.ReconcileResult(), nil } - atlasClient, orgID, err := r.AtlasProvider.Client(ctx.Context, project.ConnectionSecretObjectKey(), log) + var atlasProject *project.Project + var err error + if atlasDeployment.Spec.ExternalProjectRef != nil { + atlasProject, err = r.getProjectFromAtlas(workflowCtx, atlasDeployment) + } else { + atlasProject, err = r.getProjectFromKube(workflowCtx, atlasDeployment) + } if err != nil { - result = workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error()) - ctx.SetConditionFromResult(api.DeploymentReadyType, result) - return result.ReconcileResult(), nil + return r.terminate(workflowCtx, workflow.AtlasAPIAccessNotConfigured, err) } - ctx.OrgID = orgID - ctx.Client = atlasClient - atlasSdkClient, _, err := r.AtlasProvider.SdkClient(ctx.Context, project.ConnectionSecretObjectKey(), log) - if err != nil { - result := workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error()) - ctx.SetConditionFromResult(api.DeploymentReadyType, result) + if err = validate.AtlasDeployment(atlasDeployment, r.AtlasProvider.IsCloudGov(), atlasProject.RegionUsageRestrictions); err != nil { + result = workflow.Terminate(workflow.Internal, err.Error()) + workflowCtx.SetConditionFromResult(api.ValidationSucceeded, result) return result.ReconcileResult(), nil } - ctx.SdkClient = atlasSdkClient - r.deploymentService = deployment.NewAtlasDeployments(atlasSdkClient.ClustersApi, atlasSdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) + workflowCtx.SetConditionTrue(api.ValidationSucceeded) - deploymentInAKO := deployment.NewDeployment(project.ID(), atlasDeployment) - deploymentInAtlas, err := r.deploymentService.GetDeployment(ctx.Context, project.ID(), atlasDeployment.GetDeploymentName()) + deploymentInAKO := deployment.NewDeployment(atlasProject.ID, atlasDeployment) + deploymentInAtlas, err := r.deploymentService.GetDeployment(workflowCtx.Context, atlasProject.ID, atlasDeployment.GetDeploymentName()) if err != nil { - return r.terminate(ctx, workflow.Internal, err) + return r.terminate(workflowCtx, workflow.Internal, err) } isServerless := atlasDeployment.IsServerless() @@ -172,26 +160,88 @@ func (r *AtlasDeploymentReconciler) Reconcile(context context.Context, req ctrl. switch { case existsInAtlas && wasDeleted: - return r.delete(ctx, deploymentInAKO) + return r.delete(workflowCtx, deploymentInAKO) case !existsInAtlas && wasDeleted: - return r.unmanage(ctx, atlasDeployment) + return r.unmanage(workflowCtx, atlasDeployment) case !wasDeleted && isServerless: var serverlessDeployment *deployment.Serverless if existsInAtlas { serverlessDeployment = deploymentInAtlas.(*deployment.Serverless) } - return r.handleServerlessInstance(ctx, deploymentInAKO.(*deployment.Serverless), serverlessDeployment) + return r.handleServerlessInstance(workflowCtx, deploymentInAKO.(*deployment.Serverless), serverlessDeployment) case !wasDeleted && !isServerless: var clusterDeployment *deployment.Cluster if existsInAtlas { clusterDeployment = deploymentInAtlas.(*deployment.Cluster) } - return r.handleAdvancedDeployment(ctx, deploymentInAKO.(*deployment.Cluster), clusterDeployment) + return r.handleAdvancedDeployment(workflowCtx, deploymentInAKO.(*deployment.Cluster), clusterDeployment) } return workflow.OK().ReconcileResult(), nil } +func (r *AtlasDeploymentReconciler) getProjectFromAtlas(ctx *workflow.Context, atlasDeployment *akov2.AtlasDeployment) (*project.Project, error) { + sdkClient, orgID, err := r.AtlasProvider.SdkClient( + ctx.Context, + &client.ObjectKey{Namespace: atlasDeployment.Namespace, Name: atlasDeployment.Credentials().Name}, + r.Log, + ) + if err != nil { + return nil, err + } + + ctx.SdkClient = sdkClient + ctx.OrgID = orgID + projectService := project.NewProjectAPIService(sdkClient.ProjectsApi) + r.deploymentService = deployment.NewAtlasDeployments(sdkClient.ClustersApi, sdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) + + atlasProject, err := projectService.GetProject(ctx.Context, atlasDeployment.Spec.ExternalProjectRef.ID) + if err != nil { + return nil, err + } + + // Need to still set old client for component not yet migrated + ctx.Client, _, err = r.AtlasProvider.Client( + ctx.Context, + &client.ObjectKey{Namespace: atlasDeployment.Namespace, Name: atlasDeployment.Credentials().Name}, + r.Log, + ) + if err != nil { + return nil, err + } + + return atlasProject, nil +} + +func (r *AtlasDeploymentReconciler) getProjectFromKube(ctx *workflow.Context, atlasDeployment *akov2.AtlasDeployment) (*project.Project, error) { + atlasProject := &akov2.AtlasProject{} + if err := r.Client.Get(ctx.Context, atlasDeployment.AtlasProjectObjectKey(), atlasProject); err != nil { + return nil, err + } + + credentialsSecret, err := customresource.ComputeSecret(atlasProject, atlasDeployment) + if err != nil { + return nil, err + } + + sdkClient, orgID, err := r.AtlasProvider.SdkClient(ctx.Context, credentialsSecret, r.Log) + if err != nil { + return nil, err + } + + // Need to still set old client for component not yet migrated + ctx.Client, _, err = r.AtlasProvider.Client(ctx.Context, credentialsSecret, r.Log) + if err != nil { + return nil, err + } + + ctx.SdkClient = sdkClient + ctx.OrgID = orgID + r.deploymentService = deployment.NewAtlasDeployments(sdkClient.ClustersApi, sdkClient.ServerlessInstancesApi, r.AtlasProvider.IsCloudGov()) + + return project.NewProject(atlasProject, orgID), nil +} + func (r *AtlasDeploymentReconciler) delete( ctx *workflow.Context, deployment deployment.Deployment, // this must be the original non converted deployment @@ -270,13 +320,6 @@ func (r *AtlasDeploymentReconciler) deleteConnectionStrings(ctx *workflow.Contex return nil } -func (r *AtlasDeploymentReconciler) readProjectResource(ctx context.Context, deployment *akov2.AtlasDeployment, project *akov2.AtlasProject) workflow.Result { - if err := r.Client.Get(ctx, deployment.AtlasProjectObjectKey(), project); err != nil { - return workflow.Terminate(workflow.Internal, err.Error()) - } - return workflow.OK() -} - func (r *AtlasDeploymentReconciler) removeDeletionFinalizer(context context.Context, deployment *akov2.AtlasDeployment) error { err := r.Client.Get(context, kube.ObjectKeyFromObject(deployment), deployment) if err != nil { @@ -404,12 +447,14 @@ func NewAtlasDeploymentReconciler( deletionProtection bool, logger *zap.Logger, ) *AtlasDeploymentReconciler { + suggaredLogger := logger.Named("controllers").Named("AtlasDeployment").Sugar() + return &AtlasDeploymentReconciler{ Scheme: mgr.GetScheme(), Client: mgr.GetClient(), EventRecorder: mgr.GetEventRecorderFor("AtlasDeployment"), GlobalPredicates: predicates, - Log: logger.Named("controllers").Named("AtlasDeployment").Sugar(), + Log: suggaredLogger, AtlasProvider: atlasProvider, ObjectDeletionProtection: deletionProtection, } diff --git a/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go b/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go index 264ed504ab..1d65707da0 100644 --- a/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go +++ b/pkg/controller/atlasdeployment/atlasdeployment_controller_test.go @@ -18,6 +18,7 @@ package atlasdeployment import ( "context" + "errors" "fmt" "net/http" "reflect" @@ -33,6 +34,7 @@ import ( "go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest/observer" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -46,6 +48,7 @@ import ( atlasmock "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" @@ -1090,3 +1093,334 @@ func TestFindDeploymentsForBackupSchedule(t *testing.T) { }) } } + +func TestGetProjectFromAtlas(t *testing.T) { + tests := map[string]struct { + atlasDeployment *akov2.AtlasDeployment + deploymentSecret *corev1.Secret + atlasProvider atlas.Provider + expectedErr error + }{ + "failed to create atlas client": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster0", + Namespace: "default", + }, + Spec: akov2.AtlasDeploymentSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, + }, + }, + }, + atlasProvider: &atlasmock.TestProvider{ + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return nil, "", errors.New("failed to create client") + }, + }, + expectedErr: errors.New("failed to create client"), + }, + "failed to get project from atlas": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster0", + Namespace: "default", + }, + Spec: akov2.AtlasDeploymentSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, + }, + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + projectAPI := mockadmin.NewProjectsApi(t) + projectAPI.EXPECT().GetProject(context.Background(), "project-id"). + Return(admin.GetProjectApiRequest{ApiService: projectAPI}) + projectAPI.EXPECT().GetProjectExecute(mock.AnythingOfType("admin.GetProjectApiRequest")). + Return(nil, nil, errors.New("failed to get project")) + + return &admin.APIClient{ProjectsApi: projectAPI}, "", nil + }, + }, + expectedErr: errors.New("failed to get project"), + }, + "get project": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDeploymentSpec{ + ExternalProjectRef: &akov2.ExternalProjectReference{ + ID: "project-id", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, + }, + }, + }, + deploymentSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-pass", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "password": []byte("Passw0rd!"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + projectAPI := mockadmin.NewProjectsApi(t) + projectAPI.EXPECT().GetProject(context.Background(), "project-id"). + Return(admin.GetProjectApiRequest{ApiService: projectAPI}) + projectAPI.EXPECT().GetProjectExecute(mock.AnythingOfType("admin.GetProjectApiRequest")). + Return(&admin.Group{Id: pointer.MakePtr("project-id")}, nil, nil) + + return &admin.APIClient{ProjectsApi: projectAPI}, "", nil + }, + ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) { + return &mongodbatlas.Client{}, "", nil + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testScheme := runtime.NewScheme() + assert.NoError(t, akov2.AddToScheme(testScheme)) + assert.NoError(t, corev1.AddToScheme(testScheme)) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(tt.atlasDeployment). + WithStatusSubresource(tt.atlasDeployment) + + if tt.deploymentSecret != nil { + k8sClient.WithObjects(tt.deploymentSecret) + } + + logger := zaptest.NewLogger(t).Sugar() + r := AtlasDeploymentReconciler{ + Client: k8sClient.Build(), + AtlasProvider: tt.atlasProvider, + Log: logger, + } + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + + _, err := r.getProjectFromAtlas(ctx, tt.atlasDeployment) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestGetProjectFromKube(t *testing.T) { + tests := map[string]struct { + atlasDeployment *akov2.AtlasDeployment + project *akov2.AtlasProject + projectSecret *corev1.Secret + atlasProvider atlas.Provider + expectedErr error + }{ + "failed to get project": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster0", + Namespace: "default", + Labels: map[string]string{ + "mongodb.com/atlas-resource-version": "2.4.1", + }, + }, + Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsSupportedFunc: func() bool { + return true + }, + }, + expectedErr: &k8serrors.StatusError{ + ErrStatus: metav1.Status{ + Status: "Failure", + Message: "atlasprojects.atlas.mongodb.com \"my-project\" not found", + Reason: "NotFound", + Code: 404, + Details: &metav1.StatusDetails{ + Group: "atlas.mongodb.com", + Kind: "atlasprojects", + Name: "my-project", + }, + }, + }, + }, + "failed to create atlas sdk": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + Labels: map[string]string{ + "mongodb.com/atlas-resource-version": "2.4.1", + }, + }, + Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + Namespace: "default", + }, + }, + }, + project: &akov2.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-project", + Namespace: "default", + }, + Spec: akov2.AtlasProjectSpec{ + Name: "my-project", + ConnectionSecret: &common.ResourceRefNamespaced{ + Name: "project-secret", + }, + }, + }, + projectSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "project-secret", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "publicKey": []byte("publicKey"), + "privateKey": []byte("privateKey"), + "orgID": []byte("orgID"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsSupportedFunc: func() bool { + return true + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return nil, "", errors.New("failed to create client") + }, + }, + expectedErr: errors.New("failed to create client"), + }, + "get project": { + atlasDeployment: &akov2.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user1", + Namespace: "default", + }, + Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, + LocalCredentialHolder: api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: "project-creds", + }, + }, + }, + }, + project: &akov2.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-project", + Namespace: "default", + }, + Spec: akov2.AtlasProjectSpec{ + Name: "my-project", + ConnectionSecret: &common.ResourceRefNamespaced{ + Name: "project-secret", + }, + }, + }, + projectSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "project-secret", + Namespace: "default", + Labels: map[string]string{ + "atlas.mongodb.com/type": "credentials", + }, + }, + Data: map[string][]byte{ + "publicKey": []byte("publicKey"), + "privateKey": []byte("privateKey"), + "orgID": []byte("orgID"), + }, + }, + atlasProvider: &atlasmock.TestProvider{ + IsCloudGovFunc: func() bool { + return false + }, + SdkClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*admin.APIClient, string, error) { + return &admin.APIClient{}, "", nil + }, + ClientFunc: func(secretRef *client.ObjectKey, log *zap.SugaredLogger) (*mongodbatlas.Client, string, error) { + return &mongodbatlas.Client{}, "", nil + }, + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + testScheme := runtime.NewScheme() + assert.NoError(t, akov2.AddToScheme(testScheme)) + assert.NoError(t, corev1.AddToScheme(testScheme)) + k8sClient := fake.NewClientBuilder(). + WithScheme(testScheme). + WithObjects(tt.atlasDeployment). + WithStatusSubresource(tt.atlasDeployment) + + if tt.project != nil { + k8sClient.WithObjects(tt.project) + } + + if tt.projectSecret != nil { + k8sClient.WithObjects(tt.projectSecret) + } + + logger := zaptest.NewLogger(t).Sugar() + r := AtlasDeploymentReconciler{ + Client: k8sClient.Build(), + AtlasProvider: tt.atlasProvider, + Log: logger, + } + ctx := &workflow.Context{ + Context: context.Background(), + Log: logger, + } + + _, err := r.getProjectFromKube(ctx, tt.atlasDeployment) + assert.Equal(t, tt.expectedErr, err) + }) + } +} diff --git a/pkg/controller/atlasdeployment/deployment_test.go b/pkg/controller/atlasdeployment/deployment_test.go index cdd3af2beb..62bc7e28f6 100644 --- a/pkg/controller/atlasdeployment/deployment_test.go +++ b/pkg/controller/atlasdeployment/deployment_test.go @@ -14,7 +14,7 @@ func CreateBasicDeployment(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: "my-project", }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ diff --git a/pkg/controller/atlasdeployment/serverless_deployment_test.go b/pkg/controller/atlasdeployment/serverless_deployment_test.go index ee4c30a97c..98cf3632dd 100644 --- a/pkg/controller/atlasdeployment/serverless_deployment_test.go +++ b/pkg/controller/atlasdeployment/serverless_deployment_test.go @@ -23,6 +23,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/provider" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/workflow" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/indexer" @@ -476,6 +477,9 @@ func TestHandleServerlessInstance(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, ServerlessSpec: &akov2.ServerlessSpec{ Name: "instance0", ProviderSettings: &akov2.ServerlessProviderSettingsSpec{ @@ -555,6 +559,9 @@ func TestHandleServerlessInstance(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, ServerlessSpec: &akov2.ServerlessSpec{ Name: "instance0", ProviderSettings: &akov2.ServerlessProviderSettingsSpec{ @@ -639,6 +646,9 @@ func TestHandleServerlessInstance(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, ServerlessSpec: &akov2.ServerlessSpec{ Name: "instance0", ProviderSettings: &akov2.ServerlessProviderSettingsSpec{ @@ -708,6 +718,9 @@ func TestHandleServerlessInstance(t *testing.T) { Namespace: "default", }, Spec: akov2.AtlasDeploymentSpec{ + Project: &common.ResourceRefNamespaced{ + Name: "my-project", + }, ServerlessSpec: &akov2.ServerlessSpec{ Name: "instance0", ProviderSettings: &akov2.ServerlessProviderSettingsSpec{ diff --git a/test/e2e/atlas_gov_test.go b/test/e2e/atlas_gov_test.go index 6c26c197fa..a8bc661a91 100644 --- a/test/e2e/atlas_gov_test.go +++ b/test/e2e/atlas_gov_test.go @@ -437,7 +437,7 @@ var _ = Describe("Atlas for Government", Label("atlas-gov"), func() { Namespace: testData.Resources.Namespace, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: projectName, Namespace: testData.Resources.Namespace, }, @@ -620,7 +620,7 @@ var _ = Describe("Atlas for Government", Label("atlas-gov"), func() { Namespace: testData.Resources.Namespace, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: projectName, Namespace: testData.Resources.Namespace, }, diff --git a/test/helper/e2e/data/deployments.go b/test/helper/e2e/data/deployments.go index 2f73b806c4..9e694c94cc 100644 --- a/test/helper/e2e/data/deployments.go +++ b/test/helper/e2e/data/deployments.go @@ -35,7 +35,7 @@ func CreateAdvancedGeoshardedDeployment(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -84,7 +84,7 @@ func CreateServerlessDeployment(name string, providerName string, regionName str Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, ServerlessSpec: &akov2.ServerlessSpec{ @@ -105,7 +105,7 @@ func CreateBasicDeployment(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -139,7 +139,7 @@ func CreateDeploymentWithBackup(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -171,7 +171,7 @@ func CreateDeploymentWithBackup(name string) *akov2.AtlasDeployment { func NewDeploymentWithBackupSpec() akov2.AtlasDeploymentSpec { return akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -230,7 +230,7 @@ func CreateDeploymentWithMultiregion(name string, providerName provider.Provider Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -276,7 +276,7 @@ func CreateFreeAdvancedDeployment(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ @@ -317,7 +317,7 @@ func CreateAdvancedDeployment(name string) *akov2.AtlasDeployment { Name: name, }, Spec: akov2.AtlasDeploymentSpec{ - Project: common.ResourceRefNamespaced{ + Project: &common.ResourceRefNamespaced{ Name: ProjectName, }, DeploymentSpec: &akov2.AdvancedDeploymentSpec{ diff --git a/test/int/deployment_test.go b/test/int/deployment_test.go index 35cf458eeb..a60a7db2f2 100644 --- a/test/int/deployment_test.go +++ b/test/int/deployment_test.go @@ -1290,6 +1290,32 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment", "deployment- }) }) }) + + Describe("Manage an independent deployment resource", func() { + It("Successfully creates and manage a independent CRD", func(ctx context.Context) { + createdDeployment = akov2.DefaultAWSDeployment(namespace.Name, createdProject.Name). + Lightweight() + + By("Failing to create deployment with duplicated reference to the project", func() { + createdDeployment.Spec.ExternalProjectRef = &akov2.ExternalProjectReference{ + ID: createdProject.ID(), + } + createdDeployment.Spec.LocalCredentialHolder = api.LocalCredentialHolder{ + ConnectionSecret: &api.LocalObjectReference{ + Name: createdProject.Spec.ConnectionSecret.Name, + }, + } + + Expect(k8sClient.Create(ctx, createdDeployment)).ToNot(Succeed()) + }) + + By("Creating a independent deployment resource", func() { + createdDeployment.Spec.Project = nil + performCreate(createdDeployment, 30*time.Minute) + doDeploymentStatusChecks() + }) + }) + }) }) var _ = Describe("AtlasDeployment", Ordered, Label("int", "AtlasDeployment", "deployment-backups"), func() {