diff --git a/apis/core/v1alpha1/annotations.go b/apis/core/v1alpha1/annotations.go index 75869bb..82fddd7 100644 --- a/apis/core/v1alpha1/annotations.go +++ b/apis/core/v1alpha1/annotations.go @@ -81,4 +81,13 @@ const ( // the resource is read-only and should not be created/patched/deleted by the // ACK service controller. AnnotationReadOnly = AnnotationPrefix + "read-only" + // AnnotationForceAdoption is an annotation whose value is the identifier for whether + // we will force adoption or not. If this annotation is set to true on a CR, that + // means the user is indicating to the ACK service controller that it should + // force adoption of the resource. + AnnotationForceAdoption = AnnotationPrefix + "force-adoption" + // AnnotationAdoptionFields is an annotation whose value contains a json-like + // format of the requied fields to do a ReadOne when attempting to force-adopt + // a Resource + AnnotationAdoptionFields = AnnotationPrefix + "adoption-fields" ) diff --git a/mocks/pkg/types/aws_resource.go b/mocks/pkg/types/aws_resource.go index 29cd765..7d14bfc 100644 --- a/mocks/pkg/types/aws_resource.go +++ b/mocks/pkg/types/aws_resource.go @@ -131,6 +131,20 @@ func (_m *AWSResource) SetIdentifiers(_a0 *v1alpha1.AWSIdentifiers) error { return r0 } +// PopulateResourceFromAnnotation provides a mock function with given fields: _a0 +func (_m *AWSResource) PopulateResourceFromAnnotation(_a0 map[string]string) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(map[string]string) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SetObjectMeta provides a mock function with given fields: meta func (_m *AWSResource) SetObjectMeta(meta v1.ObjectMeta) { _m.Called(meta) diff --git a/pkg/featuregate/features.go b/pkg/featuregate/features.go index 79ce3fd..0e53ac9 100644 --- a/pkg/featuregate/features.go +++ b/pkg/featuregate/features.go @@ -19,6 +19,10 @@ package featuregate import "fmt" const ( + // ForcedAdoptResources is a feature gate for enabling forced adoption of resources + // by annotation + ForcedAdoptResources = "ForcedAdoptResources" + // ReadOnlyResources is a feature gate for enabling ReadOnly resources annotation. ReadOnlyResources = "ReadOnlyResources" @@ -32,9 +36,10 @@ const ( // defaultACKFeatureGates is a map of feature names to Feature structs // representing the default feature gates for ACK controllers. var defaultACKFeatureGates = FeatureGates{ - ReadOnlyResources: {Stage: Alpha, Enabled: false}, - TeamLevelCARM: {Stage: Alpha, Enabled: false}, - ServiceLevelCARM: {Stage: Alpha, Enabled: false}, + ForcedAdoptResources: {Stage: Alpha, Enabled: false}, + ReadOnlyResources: {Stage: Alpha, Enabled: false}, + TeamLevelCARM: {Stage: Alpha, Enabled: false}, + ServiceLevelCARM: {Stage: Alpha, Enabled: false}, } // FeatureStage represents the development stage of a feature. diff --git a/pkg/runtime/reconciler.go b/pkg/runtime/reconciler.go index 6a4f43a..d159d51 100644 --- a/pkg/runtime/reconciler.go +++ b/pkg/runtime/reconciler.go @@ -360,6 +360,82 @@ func (r *resourceReconciler) Sync( isAdopted := IsAdopted(desired) rlog.WithValues("is_adopted", isAdopted) + if r.cfg.FeatureGates.IsEnabled(featuregate.ForcedAdoptResources) { + isForcedAdoption := IsForcedAdoption(desired) + rlog.WithValues("is_forced_adoption", isForcedAdoption) + + // If the resource is being adopted by force, we need to access + // the required field passed by annotation and attempt a read. + if isForcedAdoption && NeedAdoption(desired) { + rlog.Info("Adopting Resource") + extractedFields, err := ExtractAdoptionFields(desired) + if err != nil { + return desired, err + } + if extractedFields == nil { + // TODO(michaelhtm) figure out error here + return nil, fmt.Errorf("Failed extracting fields from annotation") + } + res := desired.DeepCopy() + err = res.PopulateResourceFromAnnotation(extractedFields) + if err != nil { + return nil, err + } + resolved := res + if hasRef, ok := extractedFields["hasReferences"]; ok && hasRef == "true" { + rlog.Enter("rm.ResolveReferences") + resolved, hasReferences, err := rm.ResolveReferences(ctx, r.apiReader, res) + rlog.Exit("rm.ResolveReferences", err) + if err != nil { + return ackcondition.WithReferencesResolvedCondition(res, err), err + } + if hasReferences { + resolved = ackcondition.WithReferencesResolvedCondition(resolved, err) + } + } + + rlog.Enter("rm.EnsureTags") + err = rm.EnsureTags(ctx, resolved, r.sc.GetMetadata()) + rlog.Exit("rm.EnsureTags", err) + if err != nil { + return resolved, err + } + rlog.Enter("rm.ReadOne") + latest, err = rm.ReadOne(ctx, resolved) + if err != nil { + return nil, err + } + + if !r.rd.IsManaged(latest) { + if err = r.setResourceManaged(ctx, rm, latest); err != nil { + return nil, err + } + + // Ensure tags again after adding the finalizer and patching the + // resource. Patching desired resource omits the controller tags + // because they are not persisted in etcd. So we again ensure + // that tags are present before performing the create operation. + rlog.Enter("rm.EnsureTags") + err = rm.EnsureTags(ctx, latest, r.sc.GetMetadata()) + rlog.Exit("rm.EnsureTags", err) + if err != nil { + return latest, err + } + } + r.rd.MarkAdopted(latest) + latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest) + if err != nil { + return latest, err + } + err = r.patchResourceStatus(ctx, desired, latest) + if err != nil { + return latest, err + } + rlog.Info("Resource Adopted") + return latest, nil + } + } + if r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources) { isReadOnly := IsReadOnly(desired) rlog.WithValues("is_read_only", isReadOnly) @@ -367,6 +443,8 @@ func (r *resourceReconciler) Sync( // NOTE(a-hilaly): When the time comes to support adopting resources // using annotations, we will need to think a little bit more about // the case where a user, wants to adopt a resource as read-only. + // + // NOTE(michaelhtm): Done, tnx :) // If the resource is read-only, we enter a different code path where we // only read the resource and patch the metadata and spec. diff --git a/pkg/runtime/util.go b/pkg/runtime/util.go index b2079e0..cd1ec6b 100644 --- a/pkg/runtime/util.go +++ b/pkg/runtime/util.go @@ -14,6 +14,7 @@ package runtime import ( + "encoding/json" "strings" corev1 "k8s.io/api/core/v1" @@ -68,3 +69,50 @@ func IsReadOnly(res acktypes.AWSResource) bool { } return false } + +// IsForcedAdoption returns true if the supploed AWSResource has an annotation +// indicating that it should be adopted +func IsForcedAdoption(res acktypes.AWSResource) bool { + mo := res.MetaObject() + if mo == nil { + // Should never happen... if it does, it's buggy code. + panic("IsForcedAdoption received resource with nil RuntimeObject") + } + for k, v := range mo.GetAnnotations() { + if k == ackv1alpha1.AnnotationForceAdoption { + return strings.ToLower(v) == "true" + } + } + return false +} + +func NeedAdoption(res acktypes.AWSResource) bool { + return IsForcedAdoption(res) && !IsAdopted(res) +} + +func ExtractAdoptionFields(res acktypes.AWSResource) (map[string]string, error) { + fields := extractFieldsFromAnnotation(res) + + extractedFields := &map[string]string{} + err := json.Unmarshal([]byte(fields), extractedFields) + if err != nil { + return nil, err + } + + return *extractedFields, nil +} + +func extractFieldsFromAnnotation(res acktypes.AWSResource) string { + mo := res.MetaObject() + if mo == nil { + // Should never happen... if it does, it's buggy code. + panic("ExtractRequiredFields received resource with nil RuntimeObject") + } + + for k, v := range mo.GetAnnotations() { + if k == ackv1alpha1.AnnotationAdoptionFields { + return v + } + } + return "" +} diff --git a/pkg/types/aws_resource.go b/pkg/types/aws_resource.go index 805c018..dd2673e 100644 --- a/pkg/types/aws_resource.go +++ b/pkg/types/aws_resource.go @@ -48,4 +48,7 @@ type AWSResource interface { SetStatus(AWSResource) // DeepCopy will return a copy of the resource DeepCopy() AWSResource + // PopulateResourceFromAnnotation will set the Spec or Status field that user + // provided from annotations + PopulateResourceFromAnnotation(fields map[string]string) error }