Skip to content

Commit

Permalink
Add addoption by annotation feature
Browse files Browse the repository at this point in the history
These changes introduce a new feature gate called `ForceAdoptResources`
which allows users to provide the read required fields in the annotation
and an empty spec, and the controller would populate the resource
and adopt from AWS
  • Loading branch information
michaelhtm committed Nov 14, 2024
1 parent b5e7fbe commit 003da8c
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 3 deletions.
9 changes: 9 additions & 0 deletions apis/core/v1alpha1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
14 changes: 14 additions & 0 deletions mocks/pkg/types/aws_resource.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions pkg/featuregate/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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.
Expand Down
78 changes: 78 additions & 0 deletions pkg/runtime/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,91 @@ 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)

// 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.
Expand Down
48 changes: 48 additions & 0 deletions pkg/runtime/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package runtime

import (
"encoding/json"
"strings"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -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 ""
}
3 changes: 3 additions & 0 deletions pkg/types/aws_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 003da8c

Please sign in to comment.