Skip to content

Commit

Permalink
CLOUDP-224541: Added terminationProtection flag (#1356)
Browse files Browse the repository at this point in the history
Added terminationProtection flag & refactored deletion logic

---------

Co-authored-by: Sergiusz Urbaniak <sergiusz.urbaniak@gmail.com>
  • Loading branch information
igor-karpukhin and s-urbaniak authored Feb 9, 2024
1 parent fd82446 commit 351ff88
Show file tree
Hide file tree
Showing 15 changed files with 154 additions and 57 deletions.
12 changes: 11 additions & 1 deletion config/crd/bases/atlas.mongodb.com_atlasdeployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,13 @@ spec:
type: object
maxItems: 50
type: array
terminationProtectionEnabled:
default: false
description: Flag that indicates whether termination protection
is enabled on the cluster. If set to true, MongoDB Cloud won't
delete the cluster. If set to false, MongoDB Cloud will delete
the cluster.
type: boolean
versionReleaseSystem:
type: string
type: object
Expand Down Expand Up @@ -594,7 +601,10 @@ spec:
type: array
terminationProtectionEnabled:
default: false
description: TerminationProtectionEnabled flag
description: Flag that indicates whether termination protection
is enabled on the cluster. If set to true, MongoDB Cloud won't
delete the cluster. If set to false, MongoDB Cloud will delete
the cluster.
type: boolean
required:
- name
Expand Down
4 changes: 2 additions & 2 deletions config/crd/bases/atlas.mongodb.com_atlasprojects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ spec:
types.
type: boolean
flowName:
description: Flowdock flow namse in lower-case letters.
description: Flowdock flow name in lower-case letters.
type: string
flowdockApiTokenRef:
description: The Flowdock personal API token. Populated
Expand Down Expand Up @@ -185,7 +185,7 @@ spec:
are sent. Populated for the SMS notifications type.
type: string
opsGenieApiKeyRef:
description: Opsgenie API Key. Populated for the OPS_GENIE
description: OpsGenie API Key. Populated for the OPS_GENIE
notifications type. If the key later becomes invalid,
Atlas sends an email to the project owner and eventually
removes the token.
Expand Down
5 changes: 4 additions & 1 deletion pkg/api/v1/atlasdeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ type AdvancedDeploymentSpec struct {
CustomZoneMapping []CustomZoneMapping `json:"customZoneMapping,omitempty"`
// +optional
ManagedNamespaces []ManagedNamespace `json:"managedNamespaces,omitempty"`
// Flag that indicates whether termination protection is enabled on the cluster. If set to true, MongoDB Cloud won't delete the cluster. If set to false, MongoDB Cloud will delete the cluster.
// +kubebuilder:default:=false
TerminationProtectionEnabled bool `json:"terminationProtectionEnabled,omitempty"`
}

// ToAtlas converts the AdvancedDeploymentSpec to native Atlas client ToAtlas format.
Expand Down Expand Up @@ -169,7 +172,7 @@ type ServerlessSpec struct {
// Serverless Backup Options
BackupOptions ServerlessBackupOptions `json:"backupOptions,omitempty"`

// TerminationProtectionEnabled flag
// Flag that indicates whether termination protection is enabled on the cluster. If set to true, MongoDB Cloud won't delete the cluster. If set to false, MongoDB Cloud will delete the cluster.
// +kubebuilder:default:=false
TerminationProtectionEnabled bool `json:"terminationProtectionEnabled,omitempty"`
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/v1/atlasdeployment_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func init() {
excludedClusterFieldsTheirs["replicationFactor"] = true

// Termination protection
excludedClusterFieldsTheirs["terminationProtectionEnabled"] = true
// excludedClusterFieldsTheirs["terminationProtectionEnabled"] = true

// Root cert type
excludedClusterFieldsTheirs["rootCertType"] = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ func (r *AtlasDatabaseUserReconciler) handleDeletion(
}
}

if customresource.IsResourceProtected(dbUser, r.ObjectDeletionProtection) {
if customresource.IsResourcePolicyKeepOrDefault(dbUser, r.ObjectDeletionProtection) {
log.Info("Not removing Atlas database user from Atlas as per configuration")

err := customresource.ManageFinalizer(ctx, r.Client, dbUser, customresource.UnsetFinalizer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (r *AtlasDataFederationReconciler) Reconcile(context context.Context, req c

if !dataFederation.GetDeletionTimestamp().IsZero() {
if customresource.HaveFinalizer(dataFederation, customresource.FinalizerLabel) {
if customresource.IsResourceProtected(dataFederation, r.ObjectDeletionProtection) {
if customresource.IsResourcePolicyKeepOrDefault(dataFederation, r.ObjectDeletionProtection) {
log.Info("Not removing AtlasDataFederation from Atlas as per configuration")
} else {
if err = r.deleteDataFederationFromAtlas(context, atlasClient, dataFederation, project, log); err != nil {
Expand Down
71 changes: 41 additions & 30 deletions pkg/controller/atlasdeployment/atlasdeployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,40 +272,51 @@ func (r *AtlasDeploymentReconciler) handleDeletion(
return true, workflow.Terminate(workflow.Internal, err.Error())
}
}

return false, workflow.OK()
}

if !deployment.GetDeletionTimestamp().IsZero() {
if customresource.HaveFinalizer(deployment, customresource.FinalizerLabel) {
if err := r.cleanupBindings(workflowCtx.Context, deployment); err != nil {
result := workflow.Terminate(workflow.Internal, err.Error())
log.Errorw("failed to cleanup deployment bindings (backups)", "error", err)
return true, result
}
isProtected := customresource.IsResourceProtected(deployment, r.ObjectDeletionProtection)
if isProtected {
log.Info("Not removing Atlas deployment from Atlas as per configuration")
} else {
if customresource.ResourceShouldBeLeftInAtlas(deployment) {
log.Infof("Not removing Atlas Deployment from Atlas as the '%s' annotation is set", customresource.ResourcePolicyAnnotation)
} else {
if err := r.deleteDeploymentFromAtlas(workflowCtx, log, project, deployment); err != nil {
log.Errorf("failed to remove deployment from Atlas: %s", err)
result := workflow.Terminate(workflow.Internal, err.Error())
workflowCtx.SetConditionFromResult(status.DeploymentReadyType, result)
return true, result
}
}
}
err := customresource.ManageFinalizer(workflowCtx.Context, r.Client, deployment, customresource.UnsetFinalizer)
if err != nil {
result := workflow.Terminate(workflow.Internal, err.Error())
log.Errorw("failed to remove finalizer", "error", err)
return true, result
}
}
if !customresource.HaveFinalizer(deployment, customresource.FinalizerLabel) {
return true, prevResult
}
return false, workflow.OK()

if err := r.cleanupBindings(workflowCtx.Context, deployment); err != nil {
result := workflow.Terminate(workflow.Internal, err.Error())
log.Errorw("failed to cleanup deployment bindings (backups)", "error", err)
return true, result
}

switch {
case customresource.IsResourcePolicyKeepOrDefault(deployment, r.ObjectDeletionProtection):
log.Info("Not removing Atlas deployment from Atlas as per configuration")
case customresource.IsResourcePolicyKeep(deployment):
log.Infof("Not removing Atlas deployment from Atlas as the '%s' annotation is set", customresource.ResourcePolicyAnnotation)
case isTerminationProtectionEnabled(deployment):
msg := fmt.Sprintf("Termination protection for %s deployment enabled. Deployment in Atlas won't be removed", deployment.GetName())
log.Info(msg)
r.EventRecorder.Event(deployment, "Warning", "AtlasDeploymentTermination", msg)
default:
if err := r.deleteDeploymentFromAtlas(workflowCtx, log, project, deployment); err != nil {
log.Errorf("failed to remove deployment from Atlas: %s", err)
result := workflow.Terminate(workflow.Internal, err.Error())
workflowCtx.SetConditionFromResult(status.DeploymentReadyType, result)
return true, result
}
}

if err := customresource.ManageFinalizer(workflowCtx.Context, r.Client, deployment, customresource.UnsetFinalizer); err != nil {
result := workflow.Terminate(workflow.Internal, err.Error())
log.Errorw("failed to remove finalizer", "error", err)
return true, result
}

return true, prevResult
}

func isTerminationProtectionEnabled(deployment *mdbv1.AtlasDeployment) bool {
return (deployment.Spec.DeploymentSpec != nil &&
deployment.Spec.DeploymentSpec.TerminationProtectionEnabled) || (deployment.Spec.ServerlessSpec != nil &&
deployment.Spec.ServerlessSpec.TerminationProtectionEnabled)
}

func (r *AtlasDeploymentReconciler) cleanupBindings(context context.Context, deployment *mdbv1.AtlasDeployment) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/atlasproject/atlasproject_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func (r *AtlasProjectReconciler) ensureDeletionFinalizer(workflowCtx *workflow.C

if !project.GetDeletionTimestamp().IsZero() {
if customresource.HaveFinalizer(project, customresource.FinalizerLabel) {
if customresource.IsResourceProtected(project, r.ObjectDeletionProtection) {
if customresource.IsResourcePolicyKeepOrDefault(project, r.ObjectDeletionProtection) {
log.Info("Not removing Project from Atlas as per configuration")
result = workflow.OK()
} else {
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/atlasproject/team_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (r *AtlasProjectReconciler) teamReconcile(
if !team.GetDeletionTimestamp().IsZero() {
if customresource.HaveFinalizer(team, customresource.FinalizerLabel) {
log.Warnf("team %s is assigned to a project. Remove it from all projects before delete", team.Name)
} else if customresource.IsResourceProtected(team, r.ObjectDeletionProtection) {
} else if customresource.IsResourcePolicyKeepOrDefault(team, r.ObjectDeletionProtection) {
log.Info("Not removing Team from Atlas as per configuration")
return workflow.OK().ReconcileResult(), nil
} else {
Expand Down
12 changes: 10 additions & 2 deletions pkg/controller/customresource/customresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,16 @@ func MarkReconciliationStarted(client client.Client, resource mdbv1.AtlasCustomR
return ctx
}

// ResourceShouldBeLeftInAtlas returns 'true' if the resource should not be removed from Atlas on K8s resource removal.
func ResourceShouldBeLeftInAtlas(resource mdbv1.AtlasCustomResource) bool {
func IsResourcePolicyKeepOrDefault(resource mdbv1.AtlasCustomResource, protectionFlag bool) bool {
if policy, ok := resource.GetAnnotations()[ResourcePolicyAnnotation]; ok {
return policy == ResourcePolicyKeep
}

return protectionFlag
}

// IsResourcePolicyKeep returns 'true' if the resource should not be removed from Atlas on K8s resource removal.
func IsResourcePolicyKeep(resource mdbv1.AtlasCustomResource) bool {
if v, ok := resource.GetAnnotations()[ResourcePolicyAnnotation]; ok {
return v == ResourcePolicyKeep
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/controller/customresource/customresource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ import (

func TestResourceShouldBeLeftInAtlas(t *testing.T) {
t.Run("Empty annotations", func(t *testing.T) {
assert.False(t, ResourceShouldBeLeftInAtlas(&v1.AtlasDatabaseUser{}))
assert.False(t, IsResourcePolicyKeep(&v1.AtlasDatabaseUser{}))
})

t.Run("Other annotations", func(t *testing.T) {
assert.False(t, ResourceShouldBeLeftInAtlas(&v1.AtlasDatabaseUser{
assert.False(t, IsResourcePolicyKeep(&v1.AtlasDatabaseUser{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{"foo": "bar"},
},
}))
})

t.Run("Annotation present, resources should be removed", func(t *testing.T) {
assert.False(t, ResourceShouldBeLeftInAtlas(&v1.AtlasDatabaseUser{
assert.False(t, IsResourcePolicyKeep(&v1.AtlasDatabaseUser{
ObjectMeta: metav1.ObjectMeta{
// Any other value except for "keep" is considered as "purge"
Annotations: map[string]string{ResourcePolicyAnnotation: "foobar"},
Expand All @@ -36,7 +36,7 @@ func TestResourceShouldBeLeftInAtlas(t *testing.T) {
})

t.Run("Annotation present, resources should be kept", func(t *testing.T) {
assert.True(t, ResourceShouldBeLeftInAtlas(&v1.AtlasDatabaseUser{
assert.True(t, IsResourcePolicyKeep(&v1.AtlasDatabaseUser{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{ResourcePolicyAnnotation: ResourcePolicyKeep},
},
Expand Down
8 changes: 0 additions & 8 deletions pkg/controller/customresource/protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@ func IsOwner(resource mdbv1.AtlasCustomResource, protectionFlag bool, operatorCh
return !existInAtlas, nil
}

func IsResourceProtected(resource mdbv1.AtlasCustomResource, protectionFlag bool) bool {
if policy, ok := resource.GetAnnotations()[ResourcePolicyAnnotation]; ok {
return policy == ResourcePolicyKeep
}

return protectionFlag
}

func ApplyLastConfigApplied(ctx context.Context, resource mdbv1.AtlasCustomResource, k8sClient client.Client) error {
uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(resource)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/customresource/protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestIsResourceProtected(t *testing.T) {
}
for _, tc := range tests {
t.Run(tc.title, func(t *testing.T) {
assert.Equal(t, tc.expectedProtected, customresource.IsResourceProtected(tc.resource, tc.protectionFlag))
assert.Equal(t, tc.expectedProtected, customresource.IsResourcePolicyKeepOrDefault(tc.resource, tc.protectionFlag))
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/int/datafederation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ var _ = Describe("AtlasDataFederation", Label("AtlasDataFederation"), func() {
By("Removing Atlas DataFederation " + createdDataFederation.Name)
Expect(k8sClient.Delete(context.Background(), createdDataFederation)).To(Succeed())
deploymentName := createdDataFederation.Name
if customresource.ResourceShouldBeLeftInAtlas(createdDataFederation) || customresource.ReconciliationShouldBeSkipped(createdDataFederation) {
if customresource.IsResourcePolicyKeep(createdDataFederation) || customresource.ReconciliationShouldBeSkipped(createdDataFederation) {
By("Removing Atlas DataFederation " + createdDataFederation.Name + " from Atlas manually")
Expect(deleteAtlasDataFederation(createdProject.Status.ID, deploymentName)).To(Succeed())
}
Expand Down
77 changes: 75 additions & 2 deletions test/int/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,79 @@ var _ = Describe("AtlasDeployment", Label("int", "AtlasDeployment", "deployment-
lastGeneration++
}

Describe("Deployment with Termination Protection should remain in Atlas after the CR is deleted", Label("dedicated-termination-protection", "slow"), func() {
It("Should succeed", func() {
createdDeployment = mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name)

By(fmt.Sprintf("Creating the Deployment %s", kube.ObjectKeyFromObject(createdDeployment)), func() {
createdDeployment.Spec.DeploymentSpec.TerminationProtectionEnabled = true

performCreate(createdDeployment, 30*time.Minute)

doDeploymentStatusChecks()
checkAtlasState()
})

By("Removing deployment", func() {
Expect(k8sClient.Delete(context.Background(), createdDeployment)).To(Succeed())
})

By("Verifying the deployment is still in Atlas", func() {
Eventually(func(g Gomega) {
ctx, cancelF := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelF()
aCluster, _, err := atlasClient.ClustersApi.GetCluster(ctx, createdProject.ID(),
createdDeployment.GetDeploymentName()).Execute()
g.Expect(err).NotTo(HaveOccurred())
Expect(aCluster.GetName()).Should(BeEquivalentTo(createdDeployment.GetDeploymentName()))
}).WithTimeout(30 * time.Second).WithPolling(5 * time.Second)
})

By("Disabling Termination protection", func() {
ctx, cancelF := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelF()
aCluster, _, err := atlasClient.ClustersApi.GetCluster(ctx, createdProject.ID(),
createdDeployment.GetDeploymentName()).Execute()
Expect(err).NotTo(HaveOccurred())
aCluster.TerminationProtectionEnabled = pointer.MakePtr(false)
aCluster.ConnectionStrings = nil
_, _, err = atlasClient.ClustersApi.UpdateCluster(ctx, createdProject.ID(), createdDeployment.GetDeploymentName(), aCluster).Execute()
Expect(err).NotTo(HaveOccurred())
})

By("Waiting for Termination protection to be disabled", func() {
Eventually(func(g Gomega) {
ctx, cancelF := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelF()
aCluster, _, err := atlasClient.ClustersApi.GetCluster(ctx, createdProject.ID(),
createdDeployment.GetDeploymentName()).Execute()
g.Expect(err).NotTo(HaveOccurred())
g.Expect(aCluster.TerminationProtectionEnabled).NotTo(BeNil())
g.Expect(*aCluster.TerminationProtectionEnabled).To(BeFalse())
}).WithTimeout(2 * time.Minute).WithPolling(10 * time.Second)
})

By("Manually deleting the cluster", func() {
ctx, cancelF := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelF()
_, err := atlasClient.ClustersApi.DeleteCluster(ctx, createdProject.ID(),
createdDeployment.GetDeploymentName()).Execute()
Expect(err).NotTo(HaveOccurred())
createdDeployment = nil
})

By("Waiting for Deployment termination", func() {
Eventually(func(g Gomega) {
ctx, cancelF := context.WithTimeout(context.Background(), 20*time.Second)
defer cancelF()
_, resp, _ := atlasClient.ClustersApi.GetCluster(ctx, createdProject.ID(),
createdDeployment.GetDeploymentName()).Execute()
g.Expect(resp.Status).To(Equal(http.StatusNotFound))
}).WithTimeout(10 * time.Minute).WithPolling(20 * time.Second)
})
})
})

Describe("Deployment CR should exist if it is tried to delete and the token is not valid", func() {
It("Should Succeed", func() {
expectedDeployment := mdbv1.DefaultAWSDeployment(namespace.Name, createdProject.Name)
Expand Down Expand Up @@ -1628,7 +1701,7 @@ func deleteDeploymentFromKubernetes(project *mdbv1.AtlasProject, deployment *mdb
By(fmt.Sprintf("Removing Atlas Deployment %q", deployment.Name), func() {
Expect(k8sClient.Delete(context.Background(), deployment)).To(Succeed())
deploymentName := deployment.GetDeploymentName()
if customresource.ResourceShouldBeLeftInAtlas(deployment) || customresource.ReconciliationShouldBeSkipped(deployment) {
if customresource.IsResourcePolicyKeep(deployment) || customresource.ReconciliationShouldBeSkipped(deployment) {
By("Removing Atlas Deployment " + deployment.Name + " from Atlas manually")
Expect(deleteAtlasDeployment(project.Status.ID, deploymentName)).To(Succeed())
}
Expand All @@ -1639,7 +1712,7 @@ func deleteDeploymentFromKubernetes(project *mdbv1.AtlasProject, deployment *mdb
func deleteProjectFromKubernetes(project *mdbv1.AtlasProject) {
By(fmt.Sprintf("Removing Atlas Project %s", project.Status.ID), func() {
Expect(k8sClient.Delete(context.Background(), project)).To(Succeed())
Eventually(checkAtlasProjectRemoved(project.Status.ID), 60, interval).Should(BeTrue())
Eventually(checkAtlasProjectRemoved(project.Status.ID), 240, interval).Should(BeTrue())
})
}

Expand Down

0 comments on commit 351ff88

Please sign in to comment.