pkg/controller/direct/gkebackup/restoreplan_controller.go (302 lines of code) (raw):

// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // +tool:controller // proto.service: google.cloud.gkebackup.v1.BackupForGKE // proto.message: google.cloud.gkebackup.v1.RestorePlan // crd.type: GKEBackupRestorePlan // crd.version: v1alpha1 package gkebackup import ( "context" "fmt" "reflect" gcp "cloud.google.com/go/gkebackup/apiv1" pb "cloud.google.com/go/gkebackup/apiv1/gkebackuppb" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/fieldmaskpb" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" krm "github.com/GoogleCloudPlatform/k8s-config-connector/apis/gkebackup/v1alpha1" refs "github.com/GoogleCloudPlatform/k8s-config-connector/apis/refs/v1beta1" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/config" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/directbase" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/direct/registry" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceoverrides" ) func init() { registry.RegisterModel(krm.GKEBackupRestorePlanGVK, NewRestorePlanModel) } func NewRestorePlanModel(ctx context.Context, config *config.ControllerConfig) (directbase.Model, error) { return &restorePlanModel{config: *config}, nil } var _ directbase.Model = &restorePlanModel{} type restorePlanModel struct { config config.ControllerConfig } func (m *restorePlanModel) AdapterForObject(ctx context.Context, reader client.Reader, u *unstructured.Unstructured) (directbase.Adapter, error) { obj := &krm.GKEBackupRestorePlan{} if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { return nil, fmt.Errorf("error converting to %T: %w", obj, err) } id, err := krm.NewRestorePlanIdentity(ctx, reader, obj) if err != nil { return nil, err } // normalize required reference fields if obj.Spec.BackupPlanRef != nil { if _, err := obj.Spec.BackupPlanRef.NormalizedExternal(ctx, reader, obj.GetNamespace()); err != nil { return nil, err } } if obj.Spec.ClusterRef != nil { if _, err := obj.Spec.ClusterRef.NormalizedExternal(ctx, reader, obj.GetNamespace()); err != nil { return nil, err } } // Get gkebackup GCP client gcpClient, err := newGCPClient(ctx, &m.config) if err != nil { return nil, err } client, err := gcpClient.newBackupForGKEClient(ctx) if err != nil { return nil, err } return &restorePlanAdapter{ gcpClient: client, id: id, desired: obj, }, nil } func (m *restorePlanModel) AdapterForURL(ctx context.Context, url string) (directbase.Adapter, error) { // TODO: Support URLs return nil, nil } type restorePlanAdapter struct { gcpClient *gcp.BackupForGKEClient id *krm.RestorePlanIdentity desired *krm.GKEBackupRestorePlan actual *pb.RestorePlan resourceOverrides resourceoverrides.ResourceOverrides } var _ directbase.Adapter = &restorePlanAdapter{} func (a *restorePlanAdapter) Find(ctx context.Context) (bool, error) { log := klog.FromContext(ctx) log.V(2).Info("getting gkebackup restoreplan", "name", a.id) req := &pb.GetRestorePlanRequest{Name: a.id.String()} actual, err := a.gcpClient.GetRestorePlan(ctx, req) if err != nil { if direct.IsNotFound(err) { return false, nil } return false, fmt.Errorf("getting gkebackup restoreplan %q from gcp: %w", a.id.String(), err) } a.actual = actual return true, nil } func (a *restorePlanAdapter) Create(ctx context.Context, createOp *directbase.CreateOperation) error { log := klog.FromContext(ctx) log.V(2).Info("creating gkebackup restoreplan", "name", a.id) mapCtx := &direct.MapContext{} desired := a.desired.DeepCopy() resource := GKEBackupRestorePlanSpec_ToProto(mapCtx, &desired.Spec) if mapCtx.Err() != nil { return mapCtx.Err() } req := &pb.CreateRestorePlanRequest{ Parent: a.id.Parent().String(), RestorePlanId: a.id.ID(), RestorePlan: resource, } op, err := a.gcpClient.CreateRestorePlan(ctx, req) if err != nil { return fmt.Errorf("creating gkebackup restoreplan %s: %w", a.id.String(), err) } created, err := op.Wait(ctx) if err != nil { return fmt.Errorf("gkebackup restoreplan %s waiting creation: %w", a.id, err) } log.V(2).Info("successfully created gkebackup restoreplan in gcp", "name", a.id) status := &krm.GKEBackupRestorePlanStatus{} status.ObservedState = GKEBackupRestorePlanObservedState_FromProto(mapCtx, created) if mapCtx.Err() != nil { return mapCtx.Err() } status.ExternalRef = direct.LazyPtr(a.id.String()) return createOp.UpdateStatus(ctx, status, nil) } func (a *restorePlanAdapter) Update(ctx context.Context, updateOp *directbase.UpdateOperation) error { log := klog.FromContext(ctx) log.V(2).Info("updating gkebackup restoreplan", "name", a.id) mapCtx := &direct.MapContext{} desired := a.desired.DeepCopy() resource := GKEBackupRestorePlanSpec_ToProto(mapCtx, &desired.Spec) if mapCtx.Err() != nil { return mapCtx.Err() } paths := []string{} if desired.Spec.Description != nil && !reflect.DeepEqual(resource.GetDescription(), a.actual.GetDescription()) { paths = append(paths, "description") } if desired.Spec.Labels != nil && !reflect.DeepEqual(resource.GetLabels(), a.actual.GetLabels()) { paths = append(paths, "labels") } if desired.Spec.RestoreConfig != nil && !restoreConfigsEqual(resource.GetRestoreConfig(), a.actual.GetRestoreConfig()) { paths = append(paths, "restore_config") } var updated *pb.RestorePlan if len(paths) == 0 { log.V(2).Info("no field needs update", "name", a.id) updated = a.actual } else { resource.Name = a.id.String() // we need to set the name so that GCP API can identify the resource req := &pb.UpdateRestorePlanRequest{ RestorePlan: resource, UpdateMask: &fieldmaskpb.FieldMask{Paths: paths}, } op, err := a.gcpClient.UpdateRestorePlan(ctx, req) if err != nil { return fmt.Errorf("updating gkebackup restoreplan %s: %w", a.id.String(), err) } updated, err = op.Wait(ctx) if err != nil { return fmt.Errorf("gkebackup restoreplan %s waiting for update: %w", a.id, err) } log.V(2).Info("successfully updated gkebackup restoreplan", "name", a.id) } status := &krm.GKEBackupRestorePlanStatus{} status.ObservedState = GKEBackupRestorePlanObservedState_FromProto(mapCtx, updated) if mapCtx.Err() != nil { return mapCtx.Err() } status.ExternalRef = direct.LazyPtr(a.id.String()) return updateOp.UpdateStatus(ctx, status, nil) } func (a *restorePlanAdapter) Export(ctx context.Context) (*unstructured.Unstructured, error) { if a.actual == nil { return nil, fmt.Errorf("Find() not called") } u := &unstructured.Unstructured{} obj := &krm.GKEBackupRestorePlan{} mapCtx := &direct.MapContext{} obj.Spec = direct.ValueOf(GKEBackupRestorePlanSpec_FromProto(mapCtx, a.actual)) if mapCtx.Err() != nil { return nil, mapCtx.Err() } obj.Spec.ProjectRef = &refs.ProjectRef{External: a.id.Parent().ProjectID} obj.Spec.Location = a.id.Parent().Location uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) if err != nil { return nil, err } u.SetName(a.actual.Name) u.SetGroupVersionKind(krm.GKEBackupRestorePlanGVK) u.Object = uObj return u, nil } // Delete implements the Adapter interface. func (a *restorePlanAdapter) Delete(ctx context.Context, deleteOp *directbase.DeleteOperation) (bool, error) { log := klog.FromContext(ctx) log.V(2).Info("deleting gkebackup restoreplan", "name", a.id) req := &pb.DeleteRestorePlanRequest{Name: a.id.String()} op, err := a.gcpClient.DeleteRestorePlan(ctx, req) if err != nil { if direct.IsNotFound(err) { log.V(2).Info("skipping delete for non-existent RestorePlan, assuming it was already deleted", "name", a.id) return true, nil } return false, fmt.Errorf("deleting gkebackup restoreplan %s: %w", a.id.String(), err) } log.V(2).Info("successfully deleted gkebackup restoreplan", "name", a.id) err = op.Wait(ctx) if err != nil { return false, fmt.Errorf("waiting delete RestorePlan %s: %w", a.id, err) } return true, nil } func restoreConfigsEqual(a, b *pb.RestoreConfig) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } // Compare VolumeDataRestorePolicy if a.GetVolumeDataRestorePolicy() != b.GetVolumeDataRestorePolicy() { return false } // Compare ClusterResourceConflictPolicy if a.GetClusterResourceConflictPolicy() != b.GetClusterResourceConflictPolicy() { return false } // Compare NamespacedResourceRestoreMode if a.GetNamespacedResourceRestoreMode() != b.GetNamespacedResourceRestoreMode() { return false } // Compare ClusterResourceRestoreScope aScope, bScope := a.GetClusterResourceRestoreScope(), b.GetClusterResourceRestoreScope() if (aScope == nil) != (bScope == nil) { return false } if aScope != nil { if aScope.GetAllGroupKinds() != bScope.GetAllGroupKinds() { return false } if aScope.GetNoGroupKinds() != bScope.GetNoGroupKinds() { return false } // Compare SelectedGroupKinds aSelected, bSelected := aScope.GetSelectedGroupKinds(), bScope.GetSelectedGroupKinds() if len(aSelected) != len(bSelected) { return false } for i := range aSelected { if !proto.Equal(aSelected[i], bSelected[i]) { return false } } // Compare ExcludedGroupKinds aExcluded, bExcluded := aScope.GetExcludedGroupKinds(), bScope.GetExcludedGroupKinds() if len(aExcluded) != len(bExcluded) { return false } for i := range aExcluded { if !proto.Equal(aExcluded[i], bExcluded[i]) { return false } } } // Compare NamespacedResourceRestoreScope (oneof field) // We need to check which field is set in the oneof switch { case a.GetAllNamespaces(): if !b.GetAllNamespaces() { return false } case a.GetSelectedNamespaces() != nil: bSelected := b.GetSelectedNamespaces() if bSelected == nil || !proto.Equal(a.GetSelectedNamespaces(), bSelected) { return false } case a.GetSelectedApplications() != nil: bSelected := b.GetSelectedApplications() if bSelected == nil || !proto.Equal(a.GetSelectedApplications(), bSelected) { return false } case a.GetNoNamespaces(): if !b.GetNoNamespaces() { return false } case a.GetExcludedNamespaces() != nil: bExcluded := b.GetExcludedNamespaces() if bExcluded == nil || !proto.Equal(a.GetExcludedNamespaces(), bExcluded) { return false } default: // If we get here, the oneof field might not be set in a // Check if it's set in b if b.GetAllNamespaces() || b.GetSelectedNamespaces() != nil || b.GetSelectedApplications() != nil || b.GetNoNamespaces() || b.GetExcludedNamespaces() != nil { return false } } // Compare SubstitutionRules if !proto.Equal(&pb.RestoreConfig{SubstitutionRules: a.GetSubstitutionRules()}, &pb.RestoreConfig{SubstitutionRules: b.GetSubstitutionRules()}) { return false } // Compare TransformationRules if !proto.Equal(&pb.RestoreConfig{TransformationRules: a.GetTransformationRules()}, &pb.RestoreConfig{TransformationRules: b.GetTransformationRules()}) { return false } // Compare VolumeDataRestorePolicyBindings if !proto.Equal(&pb.RestoreConfig{VolumeDataRestorePolicyBindings: a.GetVolumeDataRestorePolicyBindings()}, &pb.RestoreConfig{VolumeDataRestorePolicyBindings: b.GetVolumeDataRestorePolicyBindings()}) { return false } // Compare RestoreOrder if !proto.Equal(a.GetRestoreOrder(), b.GetRestoreOrder()) { return false } return true }