Skip to content

Commit

Permalink
Added deletion protection for BackupSchedule and BackupPolicy
Browse files Browse the repository at this point in the history
  • Loading branch information
igor-karpukhin committed Sep 6, 2023
1 parent 76c4b1f commit ba5706b
Show file tree
Hide file tree
Showing 11 changed files with 586 additions and 51 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/google/s2a-go v0.1.5 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
Expand Down Expand Up @@ -324,6 +325,9 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
Expand Down
48 changes: 48 additions & 0 deletions pkg/api/v1/atlasbackupschedule_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
package v1

import (
"strings"

"go.mongodb.org/atlas/mongodbatlas"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1/status"
Expand Down Expand Up @@ -96,6 +99,51 @@ type AtlasBackupSchedule struct {
Status status.BackupScheduleStatus `json:"status,omitempty"`
}

func (in *AtlasBackupSchedule) ToAtlas(clusterID, clusterName string, policy *AtlasBackupPolicy) *mongodbatlas.CloudProviderSnapshotBackupPolicy {
atlasPolicy := mongodbatlas.Policy{}

for _, bpItem := range policy.Spec.Items {
atlasPolicy.PolicyItems = append(atlasPolicy.PolicyItems, mongodbatlas.PolicyItem{
FrequencyInterval: bpItem.FrequencyInterval,
FrequencyType: strings.ToLower(bpItem.FrequencyType),
RetentionValue: bpItem.RetentionValue,
RetentionUnit: strings.ToLower(bpItem.RetentionUnit),
})
}

result := &mongodbatlas.CloudProviderSnapshotBackupPolicy{
ClusterName: clusterName,
ReferenceHourOfDay: &in.Spec.ReferenceHourOfDay,
ReferenceMinuteOfHour: &in.Spec.ReferenceMinuteOfHour,
RestoreWindowDays: &in.Spec.RestoreWindowDays,
UpdateSnapshots: &in.Spec.UpdateSnapshots,
Policies: []mongodbatlas.Policy{atlasPolicy},
AutoExportEnabled: &in.Spec.AutoExportEnabled,
UseOrgAndGroupNamesInExportPrefix: &in.Spec.UseOrgAndGroupNamesInExportPrefix,
CopySettings: make([]mongodbatlas.CopySetting, 0, len(in.Spec.CopySettings)),
}

if in.Spec.Export != nil {
result.Export = &mongodbatlas.Export{
ExportBucketID: in.Spec.Export.ExportBucketID,
FrequencyType: in.Spec.Export.FrequencyType,
}
}

for _, copySetting := range in.Spec.CopySettings {
result.CopySettings = append(result.CopySettings, mongodbatlas.CopySetting{
CloudProvider: copySetting.CloudProvider,
RegionName: copySetting.RegionName,
ReplicationSpecID: copySetting.ReplicationSpecID,
ShouldCopyOplogs: copySetting.ShouldCopyOplogs,
Frequencies: copySetting.Frequencies,
})
}

result.ClusterID = clusterID
return result
}

func (in *AtlasBackupSchedule) GetStatus() status.Status {
return in.Status
}
Expand Down
81 changes: 81 additions & 0 deletions pkg/api/v1/atlasbackupschedule_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package v1

import (
"testing"

"github.com/go-test/deep"
"go.mongodb.org/atlas/mongodbatlas"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/util/toptr"
)

func Test_BackupScheduleToAtlas(t *testing.T) {
testData := []struct {
name string
inSchedule *AtlasBackupSchedule
inPolicy *AtlasBackupPolicy
clusterName string
output *mongodbatlas.CloudProviderSnapshotBackupPolicy
shouldFail bool
}{
{
name: "Correct data",
inSchedule: &AtlasBackupSchedule{
Spec: AtlasBackupScheduleSpec{
AutoExportEnabled: true,
ReferenceHourOfDay: 10,
ReferenceMinuteOfHour: 10,
RestoreWindowDays: 7,
UpdateSnapshots: false,
UseOrgAndGroupNamesInExportPrefix: false,
},
},
inPolicy: &AtlasBackupPolicy{
Spec: AtlasBackupPolicySpec{
Items: []AtlasBackupPolicyItem{
{
FrequencyType: "hourly",
FrequencyInterval: 10,
RetentionUnit: "weeks",
RetentionValue: 1,
},
},
},
},
clusterName: "testCluster",
output: &mongodbatlas.CloudProviderSnapshotBackupPolicy{
ClusterID: "test-id",
ClusterName: "testCluster",
AutoExportEnabled: toptr.MakePtr(true),
ReferenceHourOfDay: toptr.MakePtr[int64](10),
ReferenceMinuteOfHour: toptr.MakePtr[int64](10),
RestoreWindowDays: toptr.MakePtr[int64](7),
UpdateSnapshots: toptr.MakePtr(false),
UseOrgAndGroupNamesInExportPrefix: toptr.MakePtr(false),
Policies: []mongodbatlas.Policy{
{
ID: "",
PolicyItems: []mongodbatlas.PolicyItem{
{
ID: "",
FrequencyType: "hourly",
FrequencyInterval: 10,
RetentionUnit: "weeks",
RetentionValue: 1,
},
},
},
},
CopySettings: []mongodbatlas.CopySetting{},
},
shouldFail: false,
},
}

for _, tt := range testData {
result := tt.inSchedule.ToAtlas(tt.output.ClusterID, tt.clusterName, tt.inPolicy)
if diff := deep.Equal(result, tt.output); diff != nil {
t.Error(diff)
}
}
}
2 changes: 2 additions & 0 deletions pkg/api/v1/atlascustomresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ var _ AtlasCustomResource = &AtlasProject{}
var _ AtlasCustomResource = &AtlasDeployment{}
var _ AtlasCustomResource = &AtlasDatabaseUser{}
var _ AtlasCustomResource = &AtlasDataFederation{}
var _ AtlasCustomResource = &AtlasBackupSchedule{}
var _ AtlasCustomResource = &AtlasBackupPolicy{}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ type AtlasDeploymentReconciler struct {
// +kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=atlasdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=atlasdeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

// +kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasbackupschedules,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=atlas.mongodb.com,resources=atlasbackupschedules/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=atlas.mongodb.com,namespace=default,resources=atlasbackupschedules,verbs=get;list;watch;create;update;patch;delete
Expand Down
104 changes: 54 additions & 50 deletions pkg/controller/atlasdeployment/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"context"
"errors"
"fmt"
"strings"
"net/http"

"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/atlas"
"github.com/mongodb/mongodb-atlas-kubernetes/pkg/controller/validate"

"github.com/google/go-cmp/cmp"
Expand All @@ -26,6 +27,10 @@ import (
mdbv1 "github.com/mongodb/mongodb-atlas-kubernetes/pkg/api/v1"
)

var errArgIsNotBackupSchedule = errors.New("failed to match resource type as AtlasBackupSchedule")

const BackupProtected = "unable to reconcile AtlasBackupSchedule: it already exists in Atlas, it was not previously managed by the operator, and the deletion protection is enabled"

func (r *AtlasDeploymentReconciler) ensureBackupScheduleAndPolicy(
ctx context.Context,
service *workflow.Context,
Expand All @@ -40,7 +45,6 @@ func (r *AtlasDeploymentReconciler) ensureBackupScheduleAndPolicy(
if err != nil {
return err
}

return nil
}

Expand All @@ -67,6 +71,42 @@ func (r *AtlasDeploymentReconciler) ensureBackupScheduleAndPolicy(
return r.updateBackupScheduleAndPolicy(ctx, service, projectID, deployment.GetDeploymentName(), bSchedule, bPolicy)
}

func backupScheduleManagedByAtlas(ctx context.Context, atlasClient mongodbatlas.Client, projectID, clusterName string, policy *mdbv1.AtlasBackupPolicy) customresource.AtlasChecker {
return func(resource mdbv1.AtlasCustomResource) (bool, error) {
backupSchedule, ok := resource.(*mdbv1.AtlasBackupSchedule)
if !ok {
return false, errArgIsNotBackupSchedule
}

atlasBS, _, err := atlasClient.CloudProviderSnapshotBackupPolicies.Get(ctx, projectID, clusterName)
if err != nil {
var apiError *mongodbatlas.ErrorResponse
if errors.As(err, &apiError) && (apiError.ErrorCode == atlas.ResourceNotFound || apiError.HTTPCode == http.StatusNotFound) {
return false, nil
}

return false, err
}

operatorBS := backupSchedule.ToAtlas(atlasBS.ClusterID, clusterName, policy)
if err != nil {
return false, err
}
if len(operatorBS.Policies) != len(atlasBS.Policies) {
return false, nil
}
if len(atlasBS.Policies) != 0 && len(operatorBS.Policies) != 0 {
operatorBS.Policies[0].ID = atlasBS.Policies[0].ID
}

isSame, err := backupSchedulesAreEqual(atlasBS, operatorBS)
if err != nil {
return true, nil
}
return !isSame, nil
}
}

func (r *AtlasDeploymentReconciler) ensureBackupSchedule(
ctx context.Context,
service *workflow.Context,
Expand Down Expand Up @@ -182,53 +222,6 @@ func (r *AtlasDeploymentReconciler) updateBackupScheduleAndPolicy(
bSchedule *mdbv1.AtlasBackupSchedule,
bPolicy *mdbv1.AtlasBackupPolicy,
) error {
// Create new backup configuration
r.Log.Debugf("updating backup configuration for the atlas deployment: %v", clusterName)

apiPolicy := mongodbatlas.Policy{}

for _, bpItem := range bPolicy.Spec.Items {
apiPolicy.PolicyItems = append(apiPolicy.PolicyItems, mongodbatlas.PolicyItem{
FrequencyInterval: bpItem.FrequencyInterval,
FrequencyType: strings.ToLower(bpItem.FrequencyType),
RetentionValue: bpItem.RetentionValue,
RetentionUnit: strings.ToLower(bpItem.RetentionUnit),
})
}

r.Log.Debugf("updating backup configuration for the atlas deployment: %v", clusterName)
apiScheduleReq := &mongodbatlas.CloudProviderSnapshotBackupPolicy{
ClusterName: clusterName,
ReferenceHourOfDay: &bSchedule.Spec.ReferenceHourOfDay,
ReferenceMinuteOfHour: &bSchedule.Spec.ReferenceMinuteOfHour,
RestoreWindowDays: &bSchedule.Spec.RestoreWindowDays,
UpdateSnapshots: &bSchedule.Spec.UpdateSnapshots,
Policies: []mongodbatlas.Policy{apiPolicy},
AutoExportEnabled: &bSchedule.Spec.AutoExportEnabled,
UseOrgAndGroupNamesInExportPrefix: &bSchedule.Spec.UseOrgAndGroupNamesInExportPrefix,
CopySettings: make([]mongodbatlas.CopySetting, 0, len(bSchedule.Spec.CopySettings)),
}

if bSchedule.Spec.Export != nil {
apiScheduleReq.Export = &mongodbatlas.Export{
ExportBucketID: bSchedule.Spec.Export.ExportBucketID,
FrequencyType: bSchedule.Spec.Export.FrequencyType,
}
}

for _, copySetting := range bSchedule.Spec.CopySettings {
apiScheduleReq.CopySettings = append(
apiScheduleReq.CopySettings,
mongodbatlas.CopySetting{
CloudProvider: copySetting.CloudProvider,
RegionName: copySetting.RegionName,
ReplicationSpecID: copySetting.ReplicationSpecID,
ShouldCopyOplogs: copySetting.ShouldCopyOplogs,
Frequencies: copySetting.Frequencies,
},
)
}

currentSchedule, response, err := service.Client.CloudProviderSnapshotBackupPolicies.Get(ctx, projectID, clusterName)
if err != nil {
errMessage := "unable to get current backup configuration for project"
Expand All @@ -242,7 +235,18 @@ func (r *AtlasDeploymentReconciler) updateBackupScheduleAndPolicy(

r.Log.Debugf("successfully received backup configuration: %v", currentSchedule)

apiScheduleReq.ClusterID = currentSchedule.ClusterID
owner, err := customresource.IsOwner(bSchedule, r.ObjectDeletionProtection, customresource.IsResourceManagedByOperator, backupScheduleManagedByAtlas(ctx, service.Client, projectID, clusterName, bPolicy))
if err != nil {
return err
}

if !owner {
return fmt.Errorf(BackupProtected)
}
r.Log.Debugf("updating backup configuration for the atlas deployment: %v", clusterName)

apiScheduleReq := bSchedule.ToAtlas(currentSchedule.ClusterID, clusterName, bPolicy)

// There is only one policy, always
apiScheduleReq.Policies[0].ID = currentSchedule.Policies[0].ID

Expand Down
Loading

0 comments on commit ba5706b

Please sign in to comment.