diff --git a/.github/workflows/test-e2e-gov.yml b/.github/workflows/test-e2e-gov.yml new file mode 100644 index 0000000000..921658b318 --- /dev/null +++ b/.github/workflows/test-e2e-gov.yml @@ -0,0 +1,70 @@ +name: E2E Gov tests + +on: + pull_request: + types: + - closed + workflow_call: + +jobs: + e2e-gov: + name: E2E Gov tests + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + submodules: true + - name: Create k8s Kind Cluster + if: ${{ !env.ACT }} + uses: helm/kind-action@v1.8.0 + with: + version: v0.20.0 + config: test/e2e/config/kind.yaml + cluster_name: "atlas-gov-e2e-test" + wait: 180s + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version-file: "${{ github.workspace }}/go.mod" + - name: Install dependencies + run: | + go install golang.org/x/tools/cmd/goimports@latest + + wget https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv4.5.7/kustomize_v4.5.7_linux_amd64.tar.gz -O kustomize.tar.gz -q + tar xvf kustomize.tar.gz + chmod +x kustomize && mkdir -p ./bin/ && mv kustomize ./bin/kustomize + - name: Install CRDs + run: make install + - name: Run e2e test + env: + MCLI_PUBLIC_API_KEY: ${{ secrets.ATLAS_GOV_PUBLIC_KEY }} + MCLI_PRIVATE_API_KEY: ${{ secrets.ATLAS_GOV_PRIVATE_KEY }} + MCLI_ORG_ID: ${{ secrets.ATLAS_GOV_ORG_ID}} + MCLI_OPS_MANAGER_URL: "https://cloud-qa.mongodbgov.com/" + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_ACCOUNT_ARN_LIST: ${{ secrets.AWS_ACCOUNT_ARN_LIST }} + PAGER_DUTY_SERVICE_KEY: ${{ secrets.PAGER_DUTY_SERVICE_KEY }} + run: | + go install -mod=mod github.com/onsi/ginkgo/v2/ginkgo && \ + go install github.com/onsi/gomega/... + + cd test/e2e + ginkgo labels + echo 'Running: AKO_E2E_TEST=1 ginkgo --label-filter="atlas-gov" --timeout 120m --nodes=10 --flake-attempts=1 --randomize-all --cover --v --trace --show-nodes-events --output-interceptor-mode=none' && \ + AKO_E2E_TEST=1 ginkgo --label-filter="atlas-gov" --timeout 120m --nodes=10 --flake-attempts=1 --randomize-all --cover --v --trace --show-node-events --output-interceptor-mode=none --coverpkg=github.com/mongodb/mongodb-atlas-kubernetes/pkg/... + - name: Upload operator logs + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: logs + path: test/e2e/output/** + - name: Upload test results to codecov.io + if: ${{ success() }} + uses: codecov/codecov-action@v3 + with: + files: test/e2e/coverprofile.out + name: ${{ matrix.test }} + verbose: true diff --git a/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml b/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml index 5b5ec7a00f..61e65e0cc5 100644 --- a/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml +++ b/config/crd/bases/atlas.mongodb.com_atlasprojects.yaml @@ -887,6 +887,17 @@ spec: type: string type: object type: array + regionUsageRestrictions: + default: NONE + description: RegionUsageRestrictions designate the project's AWS region + when using Atlas for Government. This parameter should not be used + with commercial Atlas. In Atlas for Government, not setting this + field (defaulting to NONE) means the project is restricted to COMMERCIAL_FEDRAMP_REGIONS_ONLY + enum: + - NONE + - GOV_REGIONS_ONLY + - COMMERCIAL_FEDRAMP_REGIONS_ONLY + type: string settings: description: Settings allow to set Project Settings for the project properties: diff --git a/pkg/api/v1/atlascustomresource.go b/pkg/api/v1/atlascustomresource.go index 385c337676..9f0985d667 100644 --- a/pkg/api/v1/atlascustomresource.go +++ b/pkg/api/v1/atlascustomresource.go @@ -18,6 +18,8 @@ type AtlasCustomResource interface { } var _ AtlasCustomResource = &AtlasProject{} + +var _ AtlasCustomResource = &AtlasTeam{} var _ AtlasCustomResource = &AtlasDeployment{} var _ AtlasCustomResource = &AtlasDatabaseUser{} var _ AtlasCustomResource = &AtlasDataFederation{} diff --git a/pkg/api/v1/atlasproject_types.go b/pkg/api/v1/atlasproject_types.go index 8265ab6ff0..c1f9050188 100644 --- a/pkg/api/v1/atlasproject_types.go +++ b/pkg/api/v1/atlasproject_types.go @@ -46,6 +46,14 @@ type AtlasProjectSpec struct { // Name is the name of the Project that is created in Atlas by the Operator if it doesn't exist yet. Name string `json:"name"` + // RegionUsageRestrictions designate the project's AWS region when using Atlas for Government. + // This parameter should not be used with commercial Atlas. + // In Atlas for Government, not setting this field (defaulting to NONE) means the project is restricted to COMMERCIAL_FEDRAMP_REGIONS_ONLY + // +kubebuilder:validation:Enum=NONE;GOV_REGIONS_ONLY;COMMERCIAL_FEDRAMP_REGIONS_ONLY + // +kubebuilder:default:=NONE + // +optional + RegionUsageRestrictions string `json:"regionUsageRestrictions,omitempty"` + // ConnectionSecret is the name of the Kubernetes Secret which contains the information about the way to connect to // Atlas (organization ID, API keys). The default Operator connection configuration will be used if not provided. // +optional diff --git a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go index 93c9302ceb..ed31083cf3 100644 --- a/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go +++ b/pkg/controller/atlasdatabaseuser/atlasdatabaseuser_controller.go @@ -106,6 +106,13 @@ func (r *AtlasDatabaseUserReconciler) Reconcile(ctx context.Context, req ctrl.Re } workflowCtx.SetConditionTrue(status.ValidationSucceeded) + if !customresource.IsResourceSupportedInDomain(databaseUser, r.AtlasDomain) { + result := workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasDatabaseUser is not supported by Atlas for government"). + WithoutRetry() + workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) + return result.ReconcileResult(), nil + } + project := &mdbv1.AtlasProject{} if result = r.readProjectResource(databaseUser, project); !result.IsOk() { workflowCtx.SetConditionFromResult(status.DatabaseUserReadyType, result) diff --git a/pkg/controller/atlasdatafederation/datafederation_controller.go b/pkg/controller/atlasdatafederation/datafederation_controller.go index 29e1dd558f..d630069290 100644 --- a/pkg/controller/atlasdatafederation/datafederation_controller.go +++ b/pkg/controller/atlasdatafederation/datafederation_controller.go @@ -84,6 +84,13 @@ func (r *AtlasDataFederationReconciler) Reconcile(contextInt context.Context, re return resourceVersionIsValid.ReconcileResult(), nil } + if !customresource.IsResourceSupportedInDomain(dataFederation, r.AtlasDomain) { + result := workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasDataFederation is not supported by Atlas for government"). + WithoutRetry() + ctx.SetConditionFromResult(status.DataFederationReadyType, result) + return result.ReconcileResult(), nil + } + project := &mdbv1.AtlasProject{} if result := r.readProjectResource(contextInt, dataFederation, project); !result.IsOk() { ctx.SetConditionFromResult(status.DataFederationReadyType, result) diff --git a/pkg/controller/atlasdeployment/atlasdeployment_controller.go b/pkg/controller/atlasdeployment/atlasdeployment_controller.go index 8cadfbe35c..b33c910159 100644 --- a/pkg/controller/atlasdeployment/atlasdeployment_controller.go +++ b/pkg/controller/atlasdeployment/atlasdeployment_controller.go @@ -120,15 +120,22 @@ func (r *AtlasDeploymentReconciler) Reconcile(context context.Context, req ctrl. return resourceVersionIsValid.ReconcileResult(), nil } - if err := validate.DeploymentSpec(deployment.Spec); err != nil { + project := &mdbv1.AtlasProject{} + if result := r.readProjectResource(context, deployment, project); !result.IsOk() { + workflowCtx.SetConditionFromResult(status.DeploymentReadyType, result) + return result.ReconcileResult(), nil + } + + if err := validate.DeploymentSpec(&deployment.Spec, customresource.IsGov(r.AtlasDomain), project.Spec.RegionUsageRestrictions); err != nil { result := workflow.Terminate(workflow.Internal, err.Error()) workflowCtx.SetConditionFromResult(status.ValidationSucceeded, result) return result.ReconcileResult(), nil } workflowCtx.SetConditionTrue(status.ValidationSucceeded) - project := &mdbv1.AtlasProject{} - if result := r.readProjectResource(context, deployment, project); !result.IsOk() { + if !customresource.IsResourceSupportedInDomain(deployment, r.AtlasDomain) { + result := workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasDeployment is not supported by Atlas for government"). + WithoutRetry() workflowCtx.SetConditionFromResult(status.DeploymentReadyType, result) return result.ReconcileResult(), nil } diff --git a/pkg/controller/atlasdeployment/backup.go b/pkg/controller/atlasdeployment/backup.go index 729def4af4..ccc17b8c30 100644 --- a/pkg/controller/atlasdeployment/backup.go +++ b/pkg/controller/atlasdeployment/backup.go @@ -131,6 +131,10 @@ func (r *AtlasDeploymentReconciler) ensureBackupSchedule( return nil, err } + if !customresource.IsResourceSupportedInDomain(bSchedule, r.AtlasDomain) { + return nil, errors.New("the AtlasBackupSchedule is not supported by Atlas for government") + } + bSchedule.UpdateStatus([]status.Condition{}, status.AtlasBackupScheduleSetDeploymentID(deployment.GetDeploymentName())) if err = r.Client.Status().Update(ctx, bSchedule); err != nil { @@ -182,6 +186,10 @@ func (r *AtlasDeploymentReconciler) ensureBackupPolicy( return nil, errors.New(errText) } + if !customresource.IsResourceSupportedInDomain(bPolicy, r.AtlasDomain) { + return nil, errors.New("the AtlasBackupPolicy is not supported by Atlas for government") + } + scheduleRef := kube.ObjectKeyFromObject(bSchedule).String() bPolicy.UpdateStatus([]status.Condition{}, status.AtlasBackupPolicySetScheduleID(scheduleRef)) diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 8ee5f46473..b230e4feba 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -121,13 +121,20 @@ func (r *AtlasProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request return resourceVersionIsValid.ReconcileResult(), nil } - if err := validate.Project(project); err != nil { + if err := validate.Project(project, customresource.IsGov(r.AtlasDomain)); err != nil { result := workflow.Terminate(workflow.Internal, err.Error()) setCondition(workflowCtx, status.ValidationSucceeded, result) return result.ReconcileResult(), nil } workflowCtx.SetConditionTrue(status.ValidationSucceeded) + if !customresource.IsResourceSupportedInDomain(project, r.AtlasDomain) { + result := workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasProject is not supported by Atlas for government"). + WithoutRetry() + setCondition(workflowCtx, status.ProjectReadyType, result) + return result.ReconcileResult(), nil + } + connection, err := atlas.ReadConnection(log, r.Client, r.GlobalAPISecret, project.ConnectionSecretObjectKey()) if err != nil { result = workflow.Terminate(workflow.AtlasCredentialsNotProvided, err.Error()) diff --git a/pkg/controller/atlasproject/project.go b/pkg/controller/atlasproject/project.go index fa77f0b85f..0c53cd6381 100644 --- a/pkg/controller/atlasproject/project.go +++ b/pkg/controller/atlasproject/project.go @@ -24,6 +24,7 @@ func (r *AtlasProjectReconciler) ensureProjectExists(ctx *workflow.Context, proj OrgID: ctx.Connection.OrgID, Name: project.Spec.Name, WithDefaultAlertsSettings: &project.Spec.WithDefaultAlertsSettings, + RegionUsageRestrictions: project.Spec.RegionUsageRestrictions, } if p, _, err = ctx.Client.Projects.Create(context.Background(), p, &mongodbatlas.CreateProjectOptions{}); err != nil { return "", workflow.Terminate(workflow.ProjectNotCreatedInAtlas, err.Error()) diff --git a/pkg/controller/atlasproject/team_reconciler.go b/pkg/controller/atlasproject/team_reconciler.go index 1b40d42667..0b7bf92337 100644 --- a/pkg/controller/atlasproject/team_reconciler.go +++ b/pkg/controller/atlasproject/team_reconciler.go @@ -57,6 +57,13 @@ func (r *AtlasProjectReconciler) teamReconcile( log.Infow("-> Starting AtlasTeam reconciliation", "spec", team.Spec) + if !customresource.IsResourceSupportedInDomain(team, r.AtlasDomain) { + result := workflow.Terminate(workflow.AtlasGovUnsupported, "the AtlasTeam is not supported by Atlas for government"). + WithoutRetry() + setCondition(teamCtx, status.ReadyType, result) + return result.ReconcileResult(), nil + } + owner, err := customresource.IsOwner(team, r.ObjectDeletionProtection, customresource.IsResourceManagedByOperator, teamsManagedByAtlas(ctx, teamCtx.Client, connection.OrgID)) if err != nil { result = workflow.Terminate(workflow.Internal, fmt.Sprintf("unable to resolve ownership for deletion protection: %s", err)) diff --git a/pkg/controller/connectionsecret/connectionsecrets.go b/pkg/controller/connectionsecret/connectionsecrets.go index 870f36f03c..11384339db 100644 --- a/pkg/controller/connectionsecret/connectionsecrets.go +++ b/pkg/controller/connectionsecret/connectionsecrets.go @@ -210,7 +210,9 @@ func GetAllServerless(ctx *workflow.Context, projectID string) ([]*mongodbatlas. func IsCloudGovDomain(ctx *workflow.Context) bool { domains := []string{ "cloudgov.mongodb.com", + "cloud.mongodbgov.com", "cloud-dev.mongodbgov.com", + "cloud-qa.mongodbgov.com", } for _, domain := range domains { diff --git a/pkg/controller/customresource/customresource.go b/pkg/controller/customresource/customresource.go index 76b6722316..cf3b0483d1 100644 --- a/pkg/controller/customresource/customresource.go +++ b/pkg/controller/customresource/customresource.go @@ -2,6 +2,8 @@ package customresource import ( "context" + "net/url" + "strings" "fmt" @@ -24,11 +26,11 @@ const ( ReconciliationPolicyAnnotation = "mongodb.com/atlas-reconciliation-policy" ResourceVersion = "app.kubernetes.io/version" ResourceVersionOverride = "mongodb.com/atlas-resource-version-policy" - - ResourcePolicyKeep = "keep" - ResourcePolicyDelete = "delete" - ReconciliationPolicySkip = "skip" - ResourceVersionAllow = "allow" + ResourcePolicyKeep = "keep" + ResourcePolicyDelete = "delete" + ReconciliationPolicySkip = "skip" + ResourceVersionAllow = "allow" + govAtlasDomain = "mongodbgov.com" ) // PrepareResource queries the Custom Resource 'request.NamespacedName' and populates the 'resource' pointer. @@ -145,3 +147,31 @@ func SetAnnotation(resource mdbv1.AtlasCustomResource, key, value string) { annot[key] = value resource.SetAnnotations(annot) } + +func IsGov(domain string) bool { + domainURL, err := url.Parse(domain) + if err != nil { + return false + } + + return strings.HasSuffix(domainURL.Hostname(), govAtlasDomain) +} + +func IsResourceSupportedInDomain(resource mdbv1.AtlasCustomResource, domain string) bool { + if !IsGov(domain) { + return true + } + + switch atlasResource := resource.(type) { + case *mdbv1.AtlasProject, *mdbv1.AtlasTeam, *mdbv1.AtlasBackupSchedule, *mdbv1.AtlasBackupPolicy, *mdbv1.AtlasDatabaseUser: + return true + case *mdbv1.AtlasDataFederation: + return false + case *mdbv1.AtlasDeployment: + if atlasResource.Spec.ServerlessSpec == nil { + return true + } + } + + return false +} diff --git a/pkg/controller/customresource/customresource_test.go b/pkg/controller/customresource/customresource_test.go index bf16acc018..437811c61b 100644 --- a/pkg/controller/customresource/customresource_test.go +++ b/pkg/controller/customresource/customresource_test.go @@ -227,3 +227,83 @@ func TestResourceVersionIsValid(t *testing.T) { }) } } + +func TestIsGov(t *testing.T) { + t.Run("should return false for invalid domain", func(t *testing.T) { + assert.False(t, IsGov("http://x:namedport")) + }) + + t.Run("should return false for commercial Atlas domain", func(t *testing.T) { + assert.False(t, IsGov("https://cloud.mongodb.com/")) + }) + + t.Run("should return true for Atlas for government domain", func(t *testing.T) { + assert.True(t, IsGov("https://cloud.mongodbgov.com/")) + }) +} + +func TestIsSupportedByCloudGov(t *testing.T) { + dataProvider := map[string]struct { + domain string + resource v1.AtlasCustomResource + expectation bool + }{ + "should return true when it's commercial Atlas": { + domain: "https://cloud.mongodb.com", + resource: &v1.AtlasDataFederation{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is Project": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasProject{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is Team": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasTeam{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is BackupSchedule": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasBackupSchedule{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is BackupPolicy": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasBackupPolicy{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is DatabaseUser": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasBackupPolicy{}, + expectation: true, + }, + "should return true when it's Atlas Gov and resource is regular Deployment": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasDeployment{ + Spec: v1.AtlasDeploymentSpec{}, + }, + expectation: true, + }, + "should return false when it's Atlas Gov and resource is DataFederation": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasDataFederation{}, + expectation: false, + }, + "should return false when it's Atlas Gov and resource is Serverless Deployment": { + domain: "https://cloud.mongodbgov.com", + resource: &v1.AtlasDeployment{ + Spec: v1.AtlasDeploymentSpec{ + ServerlessSpec: &v1.ServerlessSpec{}, + }, + }, + expectation: false, + }, + } + + for desc, data := range dataProvider { + t.Run(desc, func(t *testing.T) { + assert.Equal(t, data.expectation, IsResourceSupportedInDomain(data.resource, data.domain)) + }) + } +} diff --git a/pkg/controller/validate/validate.go b/pkg/controller/validate/validate.go index 708f8fa7b5..4f71ad1543 100644 --- a/pkg/controller/validate/validate.go +++ b/pkg/controller/validate/validate.go @@ -36,7 +36,7 @@ type googleServiceAccountKey struct { UniverseDomain string `json:"universe_domain"` } -func DeploymentSpec(deploymentSpec mdbv1.AtlasDeploymentSpec) error { +func DeploymentSpec(deploymentSpec *mdbv1.AtlasDeploymentSpec, isGov bool, regionUsageRestrictions string) error { var err error if allAreNil(deploymentSpec.AdvancedDeploymentSpec, deploymentSpec.ServerlessSpec, deploymentSpec.DeploymentSpec) { @@ -47,6 +47,12 @@ func DeploymentSpec(deploymentSpec mdbv1.AtlasDeploymentSpec) error { err = errors.Join(err, errors.New("expected exactly one of spec.deploymentSpec, spec.advancedDepploymentSpec or spec.serverlessSpec, more than one were present")) } + if isGov { + if govErr := deploymentForGov(deploymentSpec, regionUsageRestrictions); govErr != nil { + err = errors.Join(err, govErr) + } + } + if deploymentSpec.DeploymentSpec != nil { if deploymentSpec.DeploymentSpec.ProviderSettings != nil && (deploymentSpec.DeploymentSpec.ProviderSettings.InstanceSizeName == "" && deploymentSpec.DeploymentSpec.ProviderSettings.ProviderName != "SERVERLESS") { err = errors.Join(err, errors.New("must specify instanceSizeName if provider name is not SERVERLESS")) @@ -71,7 +77,41 @@ func DeploymentSpec(deploymentSpec mdbv1.AtlasDeploymentSpec) error { return err } -func Project(project *mdbv1.AtlasProject) error { +func deploymentForGov(deployment *mdbv1.AtlasDeploymentSpec, regionUsageRestrictions string) error { + var err error + + if deployment.DeploymentSpec != nil { + regionErr := validCloudGovRegion(regionUsageRestrictions, deployment.DeploymentSpec.ProviderSettings.RegionName) + if regionErr != nil { + err = errors.Join(err, fmt.Errorf("deployment in atlas for government support a restricted set of regions: %w", regionErr)) + } + } + + if deployment.AdvancedDeploymentSpec != nil { + for _, replication := range deployment.AdvancedDeploymentSpec.ReplicationSpecs { + for _, region := range replication.RegionConfigs { + regionErr := validCloudGovRegion(regionUsageRestrictions, region.RegionName) + if regionErr != nil { + err = errors.Join(err, fmt.Errorf("advanced deployment in atlas for government support a restricted set of regions: %w", regionErr)) + } + } + } + } + + return err +} + +func Project(project *mdbv1.AtlasProject, isGov bool) error { + if !isGov && project.Spec.RegionUsageRestrictions != "" && project.Spec.RegionUsageRestrictions != "NONE" { + return errors.New("regionUsageRestriction can be used only with Atlas for government") + } + + if isGov { + if err := projectForGov(project); err != nil { + return err + } + } + if err := projectIPAccessList(project.Spec.ProjectIPAccessList); err != nil { return err } @@ -87,6 +127,87 @@ func Project(project *mdbv1.AtlasProject) error { return nil } +func projectForGov(project *mdbv1.AtlasProject) error { + var err error + + if len(project.Spec.NetworkPeers) > 0 { + for _, peer := range project.Spec.NetworkPeers { + if peer.ProviderName != "AWS" { + err = errors.Join(err, errors.New("atlas for government only supports AWS provider. one or more network peers are not set to AWS")) + } + + regionErr := validCloudGovRegion(project.Spec.RegionUsageRestrictions, peer.AccepterRegionName) + if regionErr != nil { + err = errors.Join(err, fmt.Errorf("network peering in atlas for government support a restricted set of regions: %w", regionErr)) + } + } + } + + if project.Spec.EncryptionAtRest != nil { + if project.Spec.EncryptionAtRest.AzureKeyVault.Enabled != nil && *project.Spec.EncryptionAtRest.AzureKeyVault.Enabled { + err = errors.Join(err, errors.New("atlas for government only supports AWS provider. disable encryption at rest for Azure")) + } + + if project.Spec.EncryptionAtRest.GoogleCloudKms.Enabled != nil && *project.Spec.EncryptionAtRest.GoogleCloudKms.Enabled { + err = errors.Join(err, errors.New("atlas for government only supports AWS provider. disable encryption at rest for Google Cloud")) + } + + if project.Spec.EncryptionAtRest.AwsKms.Enabled != nil && *project.Spec.EncryptionAtRest.AwsKms.Enabled { + regionErr := validCloudGovRegion(project.Spec.RegionUsageRestrictions, project.Spec.EncryptionAtRest.AwsKms.Region) + if regionErr != nil { + err = errors.Join(err, fmt.Errorf("encryption at rest in atlas for government support a restricted set of regions: %w", regionErr)) + } + } + } + + if len(project.Spec.PrivateEndpoints) > 0 { + for _, pe := range project.Spec.PrivateEndpoints { + if pe.Provider != "AWS" { + err = errors.Join(err, errors.New("atlas for government only supports AWS provider. one or more private endpoints are not set to AWS")) + } + + regionErr := validCloudGovRegion(project.Spec.RegionUsageRestrictions, pe.Region) + if regionErr != nil { + err = errors.Join(err, fmt.Errorf("private endpoint in atlas for government support a restricted set of regions: %w", regionErr)) + } + } + } + + return err +} + +func validCloudGovRegion(restriction, region string) error { + fedRampRegions := map[string]struct{}{ + "US_EAST_1": {}, + "US_EAST_2": {}, + "US_WEST_1": {}, + "US_WEST_2": {}, + "us-east-1": {}, + "us-east-2": {}, + "us-west-1": {}, + "us-west-2": {}, + } + govRegions := map[string]struct{}{ + "US_GOV_EAST_1": {}, + "US_GOV_WEST_1": {}, + "us-gov-east-1": {}, + "us-gov-west-1": {}, + } + + switch restriction { + case "GOV_REGIONS_ONLY": + if _, ok := govRegions[region]; !ok { + return fmt.Errorf("%s is not part of AWS for government regions", region) + } + default: + if _, ok := fedRampRegions[region]; !ok { + return fmt.Errorf("%s is not part of AWS FedRAMP regions", region) + } + } + + return nil +} + func DatabaseUser(_ *mdbv1.AtlasDatabaseUser) error { return nil } diff --git a/pkg/controller/validate/validate_test.go b/pkg/controller/validate/validate_test.go index e3025baa73..a3674f71da 100644 --- a/pkg/controller/validate/validate_test.go +++ b/pkg/controller/validate/validate_test.go @@ -25,11 +25,11 @@ func TestClusterValidation(t *testing.T) { t.Run("Invalid cluster specs", func(t *testing.T) { t.Run("Multiple specs specified", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: &mdbv1.AdvancedDeploymentSpec{}, DeploymentSpec: &mdbv1.DeploymentSpec{}} - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("No specs specified", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: nil, DeploymentSpec: nil} - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("Instance size not empty when serverless", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: nil, DeploymentSpec: &mdbv1.DeploymentSpec{ @@ -38,7 +38,7 @@ func TestClusterValidation(t *testing.T) { ProviderName: "SERVERLESS", }, }} - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("Instance size unset when not serverless", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: nil, DeploymentSpec: &mdbv1.DeploymentSpec{ @@ -47,7 +47,7 @@ func TestClusterValidation(t *testing.T) { ProviderName: "AWS", }, }} - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("different instance sizes for advanced deployment", func(t *testing.T) { t.Run("different instance size in the same region", func(t *testing.T) { @@ -66,7 +66,7 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("different instance size in different regions", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{ @@ -89,7 +89,7 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("different instance size in different replications", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{ @@ -116,7 +116,7 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) }) t.Run("different autoscaling for advanced deployment", func(t *testing.T) { @@ -149,7 +149,7 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("different autoscaling in different replications", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{ @@ -184,20 +184,20 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.Error(t, DeploymentSpec(spec)) + assert.Error(t, DeploymentSpec(&spec, false, "NONE")) }) }) }) t.Run("Valid cluster specs", func(t *testing.T) { t.Run("Advanced cluster spec specified", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: &mdbv1.AdvancedDeploymentSpec{}, DeploymentSpec: nil} - assert.NoError(t, DeploymentSpec(spec)) - assert.Nil(t, DeploymentSpec(spec)) + assert.NoError(t, DeploymentSpec(&spec, false, "NONE")) + assert.Nil(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("Regular cluster specs specified", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{AdvancedDeploymentSpec: nil, DeploymentSpec: &mdbv1.DeploymentSpec{}} - assert.NoError(t, DeploymentSpec(spec)) - assert.Nil(t, DeploymentSpec(spec)) + assert.NoError(t, DeploymentSpec(&spec, false, "NONE")) + assert.Nil(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("Serverless Cluster", func(t *testing.T) { @@ -206,8 +206,8 @@ func TestClusterValidation(t *testing.T) { ProviderName: "SERVERLESS", }, }} - assert.NoError(t, DeploymentSpec(spec)) - assert.Nil(t, DeploymentSpec(spec)) + assert.NoError(t, DeploymentSpec(&spec, false, "NONE")) + assert.Nil(t, DeploymentSpec(&spec, false, "NONE")) }) t.Run("Advanced cluster with replication config", func(t *testing.T) { spec := mdbv1.AtlasDeploymentSpec{ @@ -250,19 +250,89 @@ func TestClusterValidation(t *testing.T) { }, }, } - assert.NoError(t, DeploymentSpec(spec)) - assert.Nil(t, DeploymentSpec(spec)) + assert.NoError(t, DeploymentSpec(&spec, false, "NONE")) + assert.Nil(t, DeploymentSpec(&spec, false, "NONE")) }) }) } +func TestDeploymentForGov(t *testing.T) { + t.Run("should fail when deployment is configured to non-gov region", func(t *testing.T) { + deploy := mdbv1.AtlasDeploymentSpec{ + DeploymentSpec: &mdbv1.DeploymentSpec{ + ProviderSettings: &mdbv1.ProviderSettingsSpec{ + RegionName: "EU_EAST_1", + }, + }, + } + + assert.ErrorContains(t, deploymentForGov(&deploy, "GOV_REGIONS_ONLY"), "deployment in atlas for government support a restricted set of regions: EU_EAST_1 is not part of AWS for government regions") + }) + + t.Run("should fail when advanced deployment is configured to non-gov region", func(t *testing.T) { + deploy := mdbv1.AtlasDeploymentSpec{ + AdvancedDeploymentSpec: &mdbv1.AdvancedDeploymentSpec{ + ReplicationSpecs: []*mdbv1.AdvancedReplicationSpec{ + { + RegionConfigs: []*mdbv1.AdvancedRegionConfig{ + { + RegionName: "EU_EAST_1", + }, + }, + }, + }, + }, + } + + assert.ErrorContains(t, deploymentForGov(&deploy, "COMMERCIAL_FEDRAMP_REGIONS_ONLY"), "advanced deployment in atlas for government support a restricted set of regions: EU_EAST_1 is not part of AWS FedRAMP regions") + }) +} + func TestProjectValidation(t *testing.T) { + t.Run("should fail when commercial Atlas sets region restriction field to GOV_REGIONS_ONLY", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + }, + } + + assert.ErrorContains(t, Project(akoProject, false), "regionUsageRestriction can be used only with Atlas for government") + }) + + t.Run("should fail when commercial Atlas sets region restriction field to COMMERCIAL_FEDRAMP_REGIONS_ONLY", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "COMMERCIAL_FEDRAMP_REGIONS_ONLY", + }, + } + + assert.ErrorContains(t, Project(akoProject, false), "regionUsageRestriction can be used only with Atlas for government") + }) + + t.Run("should not fail if commercial Atlas sets region restriction field to empty", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{}, + } + + assert.NoError(t, Project(akoProject, false)) + }) + + t.Run("should not fail if commercial Atlas sets region restriction field to NONE", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "NONE", + }, + } + + assert.NoError(t, Project(akoProject, false)) + }) + t.Run("custom roles spec", func(t *testing.T) { t.Run("empty custom roles spec", func(t *testing.T) { spec := &mdbv1.AtlasProject{ Spec: mdbv1.AtlasProjectSpec{}, } - assert.NoError(t, Project(spec)) + assert.NoError(t, Project(spec, false)) }) t.Run("valid custom roles spec", func(t *testing.T) { spec := &mdbv1.AtlasProject{ @@ -280,7 +350,7 @@ func TestProjectValidation(t *testing.T) { }, }, } - assert.NoError(t, Project(spec)) + assert.NoError(t, Project(spec, false)) }) t.Run("invalid custom roles spec", func(t *testing.T) { spec := &mdbv1.AtlasProject{ @@ -304,11 +374,161 @@ func TestProjectValidation(t *testing.T) { }, }, } - assert.Error(t, Project(spec)) + assert.Error(t, Project(spec, false)) }) }) } +func TestProjectForGov(t *testing.T) { + t.Run("should fail if there's non AWS network peering config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + NetworkPeers: []mdbv1.NetworkPeer{ + { + ProviderName: "GCP", + AccepterRegionName: "europe-west-1", + RouteTableCIDRBlock: "192.168.0.0/16", + AtlasCIDRBlock: "10.8.0.0/18", + NetworkName: "my-gcp-peer", + GCPProjectID: "my-gcp-project", + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "atlas for government only supports AWS provider. one or more network peers are not set to AWS") + }) + + t.Run("should fail if there's no gov region in network peering config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + NetworkPeers: []mdbv1.NetworkPeer{ + { + ProviderName: "AWS", + AccepterRegionName: "us-east-1", + ContainerRegion: "us-east-1", + RouteTableCIDRBlock: "192.168.0.0/16", + AtlasCIDRBlock: "10.8.0.0/22", + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "network peering in atlas for government support a restricted set of regions: us-east-1 is not part of AWS for government regions") + }) + + t.Run("should fail if there's a GCP encryption at rest config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + GoogleCloudKms: mdbv1.GoogleCloudKms{ + Enabled: toptr.MakePtr(true), + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "atlas for government only supports AWS provider. disable encryption at rest for Google Cloud") + }) + + t.Run("should fail if there's a Azure encryption at rest config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AzureKeyVault: mdbv1.AzureKeyVault{ + Enabled: toptr.MakePtr(true), + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "atlas for government only supports AWS provider. disable encryption at rest for Azure") + }) + + t.Run("should fail if there's a AWS encryption at rest config with wrong region", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + Region: "us-east-1", + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "encryption at rest in atlas for government support a restricted set of regions: us-east-1 is not part of AWS for government regions") + }) + + t.Run("should fail if there's non AWS private endpoint config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + PrivateEndpoints: []mdbv1.PrivateEndpoint{ + { + Provider: "GCP", + Region: "europe-west-1", + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "atlas for government only supports AWS provider. one or more private endpoints are not set to AWS") + }) + + t.Run("should fail if there's no gov region in private endpoint config", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "COMMERCIAL_FEDRAMP_REGIONS_ONLY", + PrivateEndpoints: []mdbv1.PrivateEndpoint{ + { + Provider: "AWS", + Region: "eu-east-1", + }, + }, + }, + } + + assert.ErrorContains(t, Project(akoProject, true), "private endpoint in atlas for government support a restricted set of regions: eu-east-1 is not part of AWS FedRAMP regions") + }) + + t.Run("should succeed if resources are properly configured", func(t *testing.T) { + akoProject := &mdbv1.AtlasProject{ + Spec: mdbv1.AtlasProjectSpec{ + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + NetworkPeers: []mdbv1.NetworkPeer{ + { + ProviderName: "AWS", + AccepterRegionName: "us-gov-east-1", + ContainerRegion: "us-gov-east-1", + RouteTableCIDRBlock: "192.168.0.0/16", + AtlasCIDRBlock: "10.8.0.0/22", + }, + }, + EncryptionAtRest: &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + Region: "us-gov-east-1", + }, + }, + PrivateEndpoints: []mdbv1.PrivateEndpoint{ + { + Provider: "AWS", + Region: "us-gov-east-1", + }, + }, + }, + } + + assert.NoError(t, Project(akoProject, true)) + }) +} + func TestBackupScheduleValidation(t *testing.T) { t.Run("auto export is enabled without export policy", func(t *testing.T) { bSchedule := &mdbv1.AtlasBackupSchedule{ diff --git a/pkg/controller/workflow/reason.go b/pkg/controller/workflow/reason.go index 36095e4648..12a560e07e 100644 --- a/pkg/controller/workflow/reason.go +++ b/pkg/controller/workflow/reason.go @@ -13,6 +13,7 @@ const ( AtlasFinalizerNotSet ConditionReason = "AtlasFinalizerNotSet" AtlasFinalizerNotRemoved ConditionReason = "AtlasFinalizerNotRemoved" AtlasDeletionProtection ConditionReason = "AtlasDeletionProtection" + AtlasGovUnsupported ConditionReason = "AtlasGovUnsupported" ) // Atlas Project reasons diff --git a/test/e2e/api/atlas/atlas.go b/test/e2e/api/atlas/atlas.go index b5e7b8a709..f3157099fc 100644 --- a/test/e2e/api/atlas/atlas.go +++ b/test/e2e/api/atlas/atlas.go @@ -15,8 +15,6 @@ import ( "github.com/mongodb-forks/digest" "go.mongodb.org/atlas/mongodbatlas" - - "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/config" ) var globalAtlas *Atlas @@ -43,7 +41,7 @@ func AClient() (Atlas, error) { return A, err } A.Client = mongodbatlas.NewClient(tc) - u, _ := url.Parse(config.AtlasHost) + u, _ := url.Parse(os.Getenv("MCLI_OPS_MANAGER_URL")) A.Client.BaseURL = u return A, nil } diff --git a/test/e2e/atlas_gov_test.go b/test/e2e/atlas_gov_test.go new file mode 100644 index 0000000000..b036516476 --- /dev/null +++ b/test/e2e/atlas_gov_test.go @@ -0,0 +1,764 @@ +package e2e_test + +import ( + "context" + "fmt" + "os" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/google/uuid" + "go.mongodb.org/atlas/mongodbatlas" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/common" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/project" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/connectionsecret" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/testutil" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/cloud" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/actions/cloudaccess" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/config" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/k8s" + "github.com/mongodb/mongodb-atlas-kubernetes/test/e2e/model" +) + +var _ = Describe("Atlas for Government", Label("atlas-gov"), func() { + var awsHelper *cloud.AwsAction + var testData *model.TestDataProvider + var managerStop context.CancelFunc + projectName := fmt.Sprintf("atlas-gov-e2e-%s", uuid.New().String()[0:6]) + clusterName := fmt.Sprintf("%s-cluster", projectName) + ctx := context.Background() + + BeforeEach(func() { + By("Setting up cloud environment", func() { + checkUpAWSEnvironment() + + aws, err := cloud.NewAWSAction(GinkgoT()) + Expect(err).ToNot(HaveOccurred()) + awsHelper = aws + }) + + By("Setting up test environment", func() { + testData = model.DataProvider( + "atlas-gov", + model.NewEmptyAtlasKeyType().CreateAsGlobalLevelKey(), + 30005, + []func(*model.TestDataProvider){}, + ) + + actions.CreateNamespaceAndSecrets(testData) + }) + + By("Setting up the operator", func() { + managerStart, err := k8s.RunManager( + k8s.WithAtlasDomain(os.Getenv("MCLI_OPS_MANAGER_URL")), + k8s.WithGlobalKey(client.ObjectKey{Namespace: testData.Resources.Namespace, Name: config.DefaultOperatorGlobalKey}), + k8s.WithNamespaces(testData.Resources.Namespace), + k8s.WithObjectDeletionProtection(false), + k8s.WithSubObjectDeletionProtection(false), + ) + Expect(err).ToNot(HaveOccurred()) + + cancelCtx, cancel := context.WithCancel(ctx) + managerStop = cancel + go func() { + err := managerStart(cancelCtx) + if err != nil { + GinkgoWriter.Write([]byte(err.Error())) + } + Expect(err).ToNot(HaveOccurred()) + }() + }) + }) + + It("Manage all supported Atlas for Government features", func() { + By("Preparing API Key for integrations", func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pager-duty-service-key", + Namespace: testData.Resources.Namespace, + Labels: map[string]string{ + connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, + }, + }, + StringData: map[string]string{"password": os.Getenv("PAGER_DUTY_SERVICE_KEY")}, + } + Expect(testData.K8SClient.Create(ctx, secret)).To(Succeed()) + }) + + By("Creating a project to be managed by the operator", func() { + akoProject := &mdbv1.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasProjectSpec{ + Name: projectName, + RegionUsageRestrictions: "NONE", + ProjectIPAccessList: []project.IPAccessList{ + { + CIDRBlock: "10.0.0.0/24", + }, + }, + Integrations: []project.Integration{ + { + Type: "PAGER_DUTY", + Region: "US", + ServiceKeyRef: common.ResourceRefNamespaced{ + Name: "pager-duty-service-key", + }, + }, + }, + MaintenanceWindow: project.MaintenanceWindow{ + DayOfWeek: 1, + HourOfDay: 20, + }, + Auditing: &mdbv1.Auditing{ + AuditAuthorizationSuccess: false, + AuditFilter: `{"$or":[{"users":[]},{"$and":[{"users":{"$elemMatch":{"$or":[{"db":"admin"}]}}},{"atype":{"$in":["authenticate","dropDatabase","createUser","dropUser","dropAllUsersFromDatabase","dropAllRolesFromDatabase","shutdown"]}}]}]}`, + Enabled: true, + }, + Settings: &mdbv1.ProjectSettings{ + IsCollectDatabaseSpecificsStatisticsEnabled: toptr.MakePtr(true), + IsDataExplorerEnabled: toptr.MakePtr(false), + IsExtendedStorageSizesEnabled: toptr.MakePtr(false), + IsPerformanceAdvisorEnabled: toptr.MakePtr(true), + IsRealtimePerformancePanelEnabled: toptr.MakePtr(true), + IsSchemaAdvisorEnabled: toptr.MakePtr(true), + }, + CustomRoles: []mdbv1.CustomRole{ + { + Name: "testRole", + InheritedRoles: nil, + Actions: []mdbv1.Action{ + { + Name: "INSERT", + Resources: []mdbv1.Resource{ + { + Database: toptr.MakePtr("testD"), + Collection: toptr.MakePtr("testCollection"), + }, + }, + }, + }, + }, + }, + }, + } + testData.Project = akoProject + + Expect(testData.K8SClient.Create(ctx, testData.Project)) + }) + + By("Project is ready", func() { + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.ProjectCustomRolesReadyType), + status.TrueCondition(status.ReadyType), + ) + + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Configuring a Team", func() { + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + + users, _, err := atlasClient.Client.AtlasUsers.List(ctx, testData.Project.ID(), &mongodbatlas.ListOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(users).ToNot(BeEmpty()) + + usernames := make([]mdbv1.TeamUser, 0, len(users)) + for _, user := range users { + usernames = append(usernames, mdbv1.TeamUser(user.Username)) + } + + akoTeam := &mdbv1.AtlasTeam{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-team", projectName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.TeamSpec{ + Name: fmt.Sprintf("%s-team", projectName), + Usernames: usernames, + }, + } + testData.Teams = []*mdbv1.AtlasTeam{akoTeam} + Expect(testData.K8SClient.Create(ctx, testData.Teams[0])) + + testData.Project.Spec.Teams = []mdbv1.Team{ + { + TeamRef: common.ResourceRefNamespaced{ + Name: fmt.Sprintf("%s-team", projectName), + Namespace: testData.Resources.Namespace, + }, + Roles: []mdbv1.TeamRole{"GROUP_READ_ONLY"}, + }, + } + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.ProjectTeamsReadyType)))) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Configuring Cloud Provider Access", func() { + assumedRoleArn, err := cloudaccess.CreateAWSIAMRole(projectName) + Expect(err).ToNot(HaveOccurred()) + + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.CloudProviderAccessRoles = []mdbv1.CloudProviderAccessRole{ + { + ProviderName: "AWS", + IamAssumedRoleArn: assumedRoleArn, + }, + } + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.CloudProviderAccessRoles).ShouldNot(BeEmpty()) + g.Expect(testData.Project.Status.CloudProviderAccessRoles[0].Status).Should(BeElementOf([2]string{status.CloudProviderAccessStatusCreated, status.CloudProviderAccessStatusFailedToAuthorize})) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + + Expect( + cloudaccess.AddAtlasStatementToAWSIAMRole( + testData.Project.Status.CloudProviderAccessRoles[0].AtlasAWSAccountArn, + testData.Project.Status.CloudProviderAccessRoles[0].AtlasAssumedRoleExternalID, + projectName, + ), + ).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.CloudProviderAccessReadyType)))) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Configuring Networking Peering", func() { + awsAccountID, err := awsHelper.GetAccountID() + Expect(err).ToNot(HaveOccurred()) + + AwsVpcID, err := awsHelper.InitNetwork(projectName, "10.0.0.0/24", "us-east-1", map[string]string{"subnet-1": "10.0.0.0/24"}, false) + Expect(err).ToNot(HaveOccurred()) + + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.NetworkPeers = []mdbv1.NetworkPeer{ + { + ProviderName: "AWS", + AccepterRegionName: "us-east-1", + AtlasCIDRBlock: "192.168.224.0/21", + AWSAccountID: awsAccountID, + RouteTableCIDRBlock: "10.0.0.0/24", + VpcID: AwsVpcID, + }, + } + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.NetworkPeers).ShouldNot(BeEmpty()) + g.Expect(testData.Project.Status.NetworkPeers[0].StatusName).Should(Equal("PENDING_ACCEPTANCE")) + }).WithTimeout(time.Minute * 15).WithPolling(time.Second * 20).Should(Succeed()) + + Expect(awsHelper.AcceptVpcPeeringConnection(testData.Project.Status.NetworkPeers[0].ConnectionID, "us-east-1")).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.NetworkPeerReadyType)))) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Configuring Encryption at Rest", func() { + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + atlasAccountARN := testData.Project.Status.CloudProviderAccessRoles[0].AtlasAWSAccountArn + awsRoleARN := testData.Project.Status.CloudProviderAccessRoles[0].IamAssumedRoleArn + atlasRoleID := testData.Project.Status.CloudProviderAccessRoles[0].RoleID + + customerMasterKeyID, err := awsHelper.CreateKMS(fmt.Sprintf("%s-kms", projectName), "us-east-1", atlasAccountARN, awsRoleARN) + Expect(err).ToNot(HaveOccurred()) + + testData.Project.Spec.EncryptionAtRest = &mdbv1.EncryptionAtRest{ + AwsKms: mdbv1.AwsKms{ + Enabled: toptr.MakePtr(true), + CustomerMasterKeyID: customerMasterKeyID, + Region: "US_EAST_1", + RoleID: atlasRoleID, + }, + } + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.EncryptionAtRestReadyType)))) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Configuring Private Endpoint", func() { + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.PrivateEndpoints = []mdbv1.PrivateEndpoint{ + { + Provider: "AWS", + Region: "us-east-1", + }, + } + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.PrivateEndpoints).ShouldNot(BeEmpty()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.PrivateEndpointServiceReadyType)))) + }).WithTimeout(time.Minute * 15).WithPolling(time.Second * 20).Should(Succeed()) + + peID, err := awsHelper.CreatePrivateEndpoint( + testData.Project.Status.PrivateEndpoints[0].ServiceName, + fmt.Sprintf("pe-%s-gov", testData.Project.Status.PrivateEndpoints[0].ID), + "us-east-1", + ) + Expect(err).ToNot(HaveOccurred()) + + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.PrivateEndpoints[0].ID = peID + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElement(testutil.MatchCondition(status.TrueCondition(status.PrivateEndpointReadyType)))) + }).WithTimeout(time.Minute * 10).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Project is in ready state", func() { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.TrueCondition(status.IPAccessListReadyType), + status.TrueCondition(status.IntegrationReadyType), + status.TrueCondition(status.MaintenanceWindowReadyType), + status.TrueCondition(status.AuditingReadyType), + status.TrueCondition(status.ProjectSettingsReadyType), + status.TrueCondition(status.ProjectCustomRolesReadyType), + status.TrueCondition(status.ProjectTeamsReadyType), + status.TrueCondition(status.CloudProviderAccessReadyType), + status.TrueCondition(status.NetworkPeerReadyType), + status.TrueCondition(status.EncryptionAtRestReadyType), + status.TrueCondition(status.ReadyType), + ) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Creating a Cluster", func() { + akoBackupPolicy := &mdbv1.AtlasBackupPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-policy", clusterName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasBackupPolicySpec{ + Items: []mdbv1.AtlasBackupPolicyItem{ + { + FrequencyType: "hourly", + FrequencyInterval: 12, + RetentionUnit: "days", + RetentionValue: 7, + }, + }, + }, + } + Expect(testData.K8SClient.Create(ctx, akoBackupPolicy)) + + akoBackupSchedule := &mdbv1.AtlasBackupSchedule{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-schedule", clusterName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasBackupScheduleSpec{ + PolicyRef: common.ResourceRefNamespaced{ + Name: fmt.Sprintf("%s-policy", clusterName), + Namespace: testData.Resources.Namespace, + }, + AutoExportEnabled: false, + Export: nil, + ReferenceHourOfDay: 22, + ReferenceMinuteOfHour: 30, + RestoreWindowDays: 7, + UpdateSnapshots: true, + UseOrgAndGroupNamesInExportPrefix: true, + CopySettings: nil, + }, + } + Expect(testData.K8SClient.Create(ctx, akoBackupSchedule)) + + akoDeployment := &mdbv1.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasDeploymentSpec{ + Project: common.ResourceRefNamespaced{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + AdvancedDeploymentSpec: &mdbv1.AdvancedDeploymentSpec{ + Name: clusterName, + BackupEnabled: toptr.MakePtr(true), + BiConnector: &mdbv1.BiConnectorSpec{ + Enabled: toptr.MakePtr(true), + ReadPreference: "secondary", + }, + ClusterType: "REPLICASET", + DiskSizeGB: toptr.MakePtr(40), + EncryptionAtRestProvider: "AWS", + Labels: []common.LabelSpec{ + {Key: "type", Value: "e2e-test"}, + {Key: "context", Value: "cloud-gov"}, + }, + MongoDBMajorVersion: "7.0", + Paused: toptr.MakePtr(false), + PitEnabled: toptr.MakePtr(true), + ReplicationSpecs: []*mdbv1.AdvancedReplicationSpec{ + { + NumShards: 1, + ZoneName: "GOV1", + RegionConfigs: []*mdbv1.AdvancedRegionConfig{ + { + ElectableSpecs: &mdbv1.Specs{ + DiskIOPS: toptr.MakePtr(int64(3000)), + InstanceSize: "M20", + NodeCount: toptr.MakePtr(3), + }, + AutoScaling: &mdbv1.AdvancedAutoScalingSpec{ + DiskGB: &mdbv1.DiskGB{ + Enabled: toptr.MakePtr(true), + }, + Compute: &mdbv1.ComputeSpec{ + Enabled: toptr.MakePtr(true), + ScaleDownEnabled: toptr.MakePtr(true), + MinInstanceSize: "M20", + MaxInstanceSize: "M40", + }, + }, + Priority: toptr.MakePtr(7), + ProviderName: "AWS", + RegionName: "US_EAST_1", + }, + }, + }, + }, + RootCertType: "ISRGROOTX1", + VersionReleaseSystem: "LTS", + }, + BackupScheduleRef: common.ResourceRefNamespaced{ + Name: fmt.Sprintf("%s-schedule", clusterName), + Namespace: testData.Resources.Namespace, + }, + ProcessArgs: &mdbv1.ProcessArgs{ + DefaultReadConcern: "available", + MinimumEnabledTLSProtocol: "TLS1_2", + JavascriptEnabled: toptr.MakePtr(true), + NoTableScan: toptr.MakePtr(false), + }, + }, + } + Expect(testData.K8SClient.Create(ctx, akoDeployment)) + }) + + By("Cluster is in ready state", func() { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.DeploymentReadyType), + status.TrueCondition(status.ReadyType), + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ResourceVersionStatus), + ) + + Eventually(func(g Gomega) { + akoDeployment := &mdbv1.AtlasDeployment{} + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: clusterName}, akoDeployment)).To(Succeed()) + g.Expect(akoDeployment.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 45).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Creating a DatabaseUser", func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-dbuser-pass", projectName), + Namespace: testData.Resources.Namespace, + Labels: map[string]string{ + connectionsecret.TypeLabelKey: connectionsecret.CredLabelVal, + }, + }, + StringData: map[string]string{"password": "myHardPass2MyDB"}, + } + Expect(testData.K8SClient.Create(ctx, secret)).To(Succeed()) + akoDBUser := &mdbv1.AtlasDatabaseUser{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-dbuser", projectName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasDatabaseUserSpec{ + Project: common.ResourceRefNamespaced{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + DatabaseName: "admin", + Labels: []common.LabelSpec{ + {Key: "type", Value: "e2e-test"}, + {Key: "context", Value: "cloud-gov"}, + }, + Roles: []mdbv1.RoleSpec{ + { + RoleName: "readAnyDatabase", + DatabaseName: "admin", + }, + }, + Scopes: []mdbv1.ScopeSpec{ + { + Name: clusterName, + Type: mdbv1.DeploymentScopeType, + }, + }, + Username: fmt.Sprintf("%s-dbuser", projectName), + PasswordSecret: &common.ResourceRef{ + Name: fmt.Sprintf("%s-dbuser-pass", projectName), + }, + }, + } + Expect(testData.K8SClient.Create(ctx, akoDBUser)) + }) + + By("DatabaseUser is in ready state", func() { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ReadyType), + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ResourceVersionStatus), + ) + + Eventually(func(g Gomega) { + akoDBUser := &mdbv1.AtlasDatabaseUser{} + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: fmt.Sprintf("%s-dbuser", projectName)}, akoDBUser)).To(Succeed()) + g.Expect(akoDBUser.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 10).WithPolling(time.Second * 20).Should(Succeed()) + }) + }) + + It("Fail to manage when there are non supported features for Atlas for Government", func() { + By("Creating a project to be managed by the operator", func() { + akoProject := &mdbv1.AtlasProject{ + ObjectMeta: metav1.ObjectMeta{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasProjectSpec{ + Name: projectName, + RegionUsageRestrictions: "GOV_REGIONS_ONLY", + }, + } + testData.Project = akoProject + + Expect(testData.K8SClient.Create(ctx, testData.Project)) + }) + + By("Project is ready", func() { + Eventually(func(g Gomega) { + expectedConditions := testutil.MatchConditions( + status.TrueCondition(status.ValidationSucceeded), + status.TrueCondition(status.ProjectReadyType), + status.TrueCondition(status.ReadyType), + ) + + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Creating a Serverless Cluster", func() { + akoDeployment := &mdbv1.AtlasDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.AtlasDeploymentSpec{ + Project: common.ResourceRefNamespaced{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + ServerlessSpec: &mdbv1.ServerlessSpec{ + Name: clusterName, + ProviderSettings: &mdbv1.ProviderSettingsSpec{ + BackingProviderName: "AWS", + ProviderName: "SERVERLESS", + RegionName: "US_GOV_EAST_1", + }, + TerminationProtectionEnabled: false, + }, + }, + } + Expect(testData.K8SClient.Create(ctx, akoDeployment)) + }) + + By("Serverless is not supported in Atlas for government", func() { + expectedConditions := testutil.MatchConditions( + status.FalseCondition(status.DeploymentReadyType), + status.FalseCondition(status.ReadyType), + status.TrueCondition(status.ValidationSucceeded), + ) + + Eventually(func(g Gomega) { + akoDeployment := &mdbv1.AtlasDeployment{} + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: clusterName}, akoDeployment)).To(Succeed()) + g.Expect(akoDeployment.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Creating a Data Federation", func() { + akoDataFederation := &mdbv1.AtlasDataFederation{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-data-federation", projectName), + Namespace: testData.Resources.Namespace, + }, + Spec: mdbv1.DataFederationSpec{ + Project: common.ResourceRefNamespaced{ + Name: projectName, + Namespace: testData.Resources.Namespace, + }, + Name: fmt.Sprintf("%s-data-federation", projectName), + Storage: &mdbv1.Storage{ + Databases: []mdbv1.Database{ + { + Name: "test-db-1", + Collections: []mdbv1.Collection{ + { + Name: "test-collection-1", + DataSources: []mdbv1.DataSource{ + { + StoreName: "http-test", + Urls: []string{ + "https://data.cityofnewyork.us/api/views/vfnx-vebw/rows.csv", + }, + }, + }, + }, + }, + }, + }, + Stores: []mdbv1.Store{ + { + Name: "http-test", + Provider: "http", + }, + }, + }, + }, + } + Expect(testData.K8SClient.Create(ctx, akoDataFederation)) + }) + + By("DataFederation is not supported in Atlas for government", func() { + expectedConditions := testutil.MatchConditions( + status.FalseCondition(status.DataFederationReadyType), + status.FalseCondition(status.ReadyType), + ) + + Eventually(func(g Gomega) { + akoDataFederation := &mdbv1.AtlasDataFederation{} + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: fmt.Sprintf("%s-data-federation", projectName)}, akoDataFederation)).To(Succeed()) + g.Expect(akoDataFederation.Status.Conditions).To(ContainElements(expectedConditions)) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + }) + + AfterEach(func() { + By("Deleting DataFederation from the operator", func() { + akoDataFederation := &mdbv1.AtlasDataFederation{} + err := testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: fmt.Sprintf("%s-data-federation", projectName)}, akoDataFederation) + if err == nil { + Expect(testData.K8SClient.Delete(ctx, akoDataFederation)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(akoDataFederation), akoDataFederation)).ToNot(Succeed()) + }).WithTimeout(time.Minute * 10).WithPolling(time.Second * 20).Should(Succeed()) + } + }) + + By("Deleting DatabaseUser from the operator", func() { + akoDBUser := &mdbv1.AtlasDatabaseUser{} + err := testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: fmt.Sprintf("%s-dbuser", projectName)}, akoDBUser) + if err == nil { + Expect(testData.K8SClient.Delete(ctx, akoDBUser)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(akoDBUser), akoDBUser)).ToNot(Succeed()) + }).WithTimeout(time.Minute * 10).WithPolling(time.Second * 20).Should(Succeed()) + } + }) + + By("Deleting cluster from the operator", func() { + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + akoDeployment := &mdbv1.AtlasDeployment{} + Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Namespace: testData.Resources.Namespace, Name: clusterName}, akoDeployment)).To(Succeed()) + Expect(testData.K8SClient.Delete(ctx, akoDeployment)).To(Succeed()) + + Eventually(func(g Gomega) { + _, _, err := atlasClient.Client.AdvancedClusters.Get(ctx, testData.Project.ID(), clusterName) + g.Expect(err).To(HaveOccurred()) + }).WithTimeout(time.Minute * 30).WithPolling(time.Second * 20).Should(Succeed()) + + if akoDeployment.Spec.BackupScheduleRef.Name != "" { + akoBackupSchedule := &mdbv1.AtlasBackupSchedule{} + Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-schedule", clusterName), Namespace: testData.Resources.Namespace}, akoBackupSchedule)).To(Succeed()) + Expect(testData.K8SClient.Delete(ctx, akoBackupSchedule)).To(Succeed()) + + akoBackupPolicy := &mdbv1.AtlasBackupPolicy{} + Expect(testData.K8SClient.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-policy", clusterName), Namespace: testData.Resources.Namespace}, akoBackupPolicy)).To(Succeed()) + Expect(testData.K8SClient.Delete(ctx, akoBackupPolicy)).To(Succeed()) + } + }) + + By("Deleting team from the operator", func() { + Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + testData.Project.Spec.Teams = nil + Expect(testData.K8SClient.Update(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).To(Succeed()) + g.Expect(testData.Project.Status.Conditions).ToNot(ContainElement(testutil.MatchCondition(status.TrueCondition(status.ProjectTeamsReadyType)))) + }).WithTimeout(time.Minute * 5).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Deleting project from the operator", func() { + Expect(testData.K8SClient.Delete(ctx, testData.Project)).To(Succeed()) + + Eventually(func(g Gomega) { + g.Expect(testData.K8SClient.Get(ctx, client.ObjectKeyFromObject(testData.Project), testData.Project)).ToNot(Succeed()) + }).WithTimeout(time.Minute * 15).WithPolling(time.Second * 20).Should(Succeed()) + }) + + By("Stopping the operator", func() { + managerStop() + }) + + By("Clean up", func() { + actions.AfterEachFinalCleanup([]model.TestDataProvider{*testData}) + }) + }) +}) diff --git a/test/e2e/k8s/operator.go b/test/e2e/k8s/operator.go index ba8423f5c0..e26933188e 100644 --- a/test/e2e/k8s/operator.go +++ b/test/e2e/k8s/operator.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlasdatafederation" + "go.uber.org/zap/zaptest" . "github.com/onsi/ginkgo/v2" @@ -150,6 +152,22 @@ func BuildManager(initCfg *Config) (manager.Manager, error) { return nil, err } + if err = (&atlasdatafederation.AtlasDataFederationReconciler{ + ResourceWatcher: watch.NewResourceWatcher(), + Client: mgr.GetClient(), + Log: logger.Named("controllers").Named("AtlasDataFederation").Sugar(), + Scheme: mgr.GetScheme(), + AtlasDomain: config.AtlasDomain, + GlobalAPISecret: config.GlobalAPISecret, + EventRecorder: mgr.GetEventRecorderFor("AtlasDataFederation"), + GlobalPredicates: globalPredicates, + ObjectDeletionProtection: config.ObjectDeletionProtection, + SubObjectDeletionProtection: config.SubObjectDeletionProtection, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AtlasDataFederation") + return nil, err + } + if err = mgr.AddHealthzCheck("health", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") return nil, err @@ -211,16 +229,22 @@ func managerDefaults() *Config { return &Config{ AtlasDomain: "https://cloud-qa.mongodb.com/", EnableLeaderElection: false, - MetricsAddr: ":8080", + MetricsAddr: "0", Namespace: "mongodb-atlas-system", WatchedNamespaces: map[string]bool{}, - ProbeAddr: ":8081", + ProbeAddr: "0", GlobalAPISecret: client.ObjectKey{}, ObjectDeletionProtection: false, SubObjectDeletionProtection: false, } } +func WithAtlasDomain(domain string) ManagerConfig { + return func(config *Config) { + config.AtlasDomain = domain + } +} + func WithNamespaces(namespaces ...string) ManagerConfig { return func(config *Config) { for _, namespace := range namespaces {