Skip to content

Commit

Permalink
support Atlas for government (#1135)
Browse files Browse the repository at this point in the history
  • Loading branch information
helderjs authored Sep 28, 2023
1 parent e7a6596 commit f014006
Show file tree
Hide file tree
Showing 20 changed files with 1,411 additions and 36 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/test-e2e-gov.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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
11 changes: 11 additions & 0 deletions config/crd/bases/atlas.mongodb.com_atlasprojects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/v1/atlascustomresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type AtlasCustomResource interface {
}

var _ AtlasCustomResource = &AtlasProject{}

var _ AtlasCustomResource = &AtlasTeam{}
var _ AtlasCustomResource = &AtlasDeployment{}
var _ AtlasCustomResource = &AtlasDatabaseUser{}
var _ AtlasCustomResource = &AtlasDataFederation{}
Expand Down
8 changes: 8 additions & 0 deletions pkg/api/v1/atlasproject_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 10 additions & 3 deletions pkg/controller/atlasdeployment/atlasdeployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/controller/atlasdeployment/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))

Expand Down
9 changes: 8 additions & 1 deletion pkg/controller/atlasproject/atlasproject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions pkg/controller/atlasproject/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
7 changes: 7 additions & 0 deletions pkg/controller/atlasproject/team_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/connectionsecret/connectionsecrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
40 changes: 35 additions & 5 deletions pkg/controller/customresource/customresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package customresource

import (
"context"
"net/url"
"strings"

"fmt"

Expand All @@ -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.
Expand Down Expand Up @@ -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
}
80 changes: 80 additions & 0 deletions pkg/controller/customresource/customresource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
Loading

0 comments on commit f014006

Please sign in to comment.