Skip to content

Commit

Permalink
Merge pull request #164 from michaelhtm/force-adoption
Browse files Browse the repository at this point in the history
Add addoption by annotation feature
  • Loading branch information
a-hilaly authored Dec 2, 2024
2 parents b5e7fbe + 4d78380 commit a6b0014
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 0 deletions.
10 changes: 10 additions & 0 deletions apis/core/v1alpha1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,14 @@ const (
// the resource is read-only and should not be created/patched/deleted by the
// ACK service controller.
AnnotationReadOnly = AnnotationPrefix + "read-only"
// AnnotationAdoptionPolicy is an annotation whose value is the identifier for whether
// we will attempt adoption only (value = adopt-only) or attempt a create if resource
// is not found (value adopt-or-create).
//
// NOTE (michaelhtm): Currently create-or-adopt is not supported
AnnotationAdoptionPolicy = AnnotationPrefix + "adoption-policy"
// 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.

5 changes: 5 additions & 0 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 (
// ResourceAdoption is a feature gate for enabling forced adoption of resources
// by annotation
ResourceAdoption = "ResourceAdoption"

// ReadOnlyResources is a feature gate for enabling ReadOnly resources annotation.
ReadOnlyResources = "ReadOnlyResources"

Expand All @@ -32,6 +36,7 @@ const (
// defaultACKFeatureGates is a map of feature names to Feature structs
// representing the default feature gates for ACK controllers.
var defaultACKFeatureGates = FeatureGates{
ResourceAdoption: {Stage: Alpha, Enabled: false},
ReadOnlyResources: {Stage: Alpha, Enabled: false},
TeamLevelCARM: {Stage: Alpha, Enabled: false},
ServiceLevelCARM: {Stage: Alpha, Enabled: false},
Expand Down
85 changes: 85 additions & 0 deletions pkg/runtime/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const (
// resource if the CARM cache is not synced yet, or if the roleARN is not
// available.
roleARNNotAvailableRequeueDelay = 15 * time.Second
// adoptOrCreate is an annotation field that decides whether to create the
// resource if it doesn't exist, or adopt the resource if it exists.
// value comes from getAdoptionPolicy
// adoptOrCreate = "adopt-or-create"
)

// reconciler describes a generic reconciler within ACK.
Expand Down Expand Up @@ -298,6 +302,70 @@ func (r *resourceReconciler) handleCacheError(
return r.HandleReconcileError(ctx, desired, latest, requeue.NeededAfter(err, roleARNNotAvailableRequeueDelay))
}

func (r *resourceReconciler) handleAdoption(
ctx context.Context,
rm acktypes.AWSResourceManager,
desired acktypes.AWSResource,
rlog acktypes.Logger,
) (acktypes.AWSResource, error) {
// If the resource is being adopted by force, we need to access
// the required field passed by annotation and attempt a read.

rlog.Info("Adopting Resource")
extractedFields, err := ExtractAdoptionFields(desired)
if err != nil {
return desired, ackerr.NewTerminalError(err)
}
if len(extractedFields) == 0 {
// TODO(michaelhtm) Here we need to figure out if we want to have an
// error or not. should we consider accepting values from Spec?
// And then we can let the ReadOne figure out if we have missing
// required fields for a Read
return nil, fmt.Errorf("failed extracting fields from annotation")
}
resolved := desired.DeepCopy()
err = resolved.PopulateResourceFromAnnotation(extractedFields)
if err != nil {
return nil, 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 latest, err
}

if err = r.setResourceManaged(ctx, rm, latest); err != nil {
return latest, 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)
rlog.WithValues("is_adopted", "true")
latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
if err != nil {
return latest, err
}

rlog.Info("Resource Adopted")
return latest, nil
}

// reconcile either cleans up a deleted resource or ensures that the supplied
// AWSResource's backing API resource matches the supplied desired state.
//
Expand Down Expand Up @@ -360,13 +428,30 @@ func (r *resourceReconciler) Sync(
isAdopted := IsAdopted(desired)
rlog.WithValues("is_adopted", isAdopted)

if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
latest, err := r.handleAdoption(ctx, rm, desired, rlog)

if err != nil {
// If we get an error, we want to return here
// TODO(michaelhtm): Change the handling of
// the error to allow Adopt or Create here
// when supported
return latest, err
}
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
53 changes: 53 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,55 @@ func IsReadOnly(res acktypes.AWSResource) bool {
}
return false
}

// GetAdoptionPolicy returns the Adoption Policy of the resource
// defined by the user in annotation. Possible values are:
// adopt-only | adopt-or-create
// adopt-only keeps requing until the resource is found
// adopt-or-create creates the resource if does not exist
func GetAdoptionPolicy(res acktypes.AWSResource) string {
mo := res.MetaObject()
if mo == nil {
panic("getAdoptionPolicy received resource with nil RuntimeObject")
}
for k, v := range mo.GetAnnotations() {
if k == ackv1alpha1.AnnotationAdoptionPolicy {
return v
}
}

return ""
}

// NeedAdoption returns true when the resource has
// adopt annotation but is not yet adopted
func NeedAdoption(res acktypes.AWSResource) bool {
return GetAdoptionPolicy(res) != "" && !IsAdopted(res)
}

func ExtractAdoptionFields(res acktypes.AWSResource) (map[string]string, error) {
fields := getAdoptionFields(res)

extractedFields := &map[string]string{}
err := json.Unmarshal([]byte(fields), extractedFields)
if err != nil {
return nil, err
}

return *extractedFields, nil
}

func getAdoptionFields(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 ""
}
53 changes: 53 additions & 0 deletions pkg/runtime/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,56 @@ func TestIsSynced(t *testing.T) {
})
require.False(ackrt.IsSynced(res))
}

func TestIsForcedAdoption(t *testing.T) {
require := require.New(t)

res := &mocks.AWSResource{}
res.On("MetaObject").Return(&metav1.ObjectMeta{
Annotations: map[string]string{
ackv1alpha1.AnnotationAdoptionPolicy: "true",
ackv1alpha1.AnnotationAdopted: "false",
},
})
require.True(ackrt.NeedAdoption(res))

res = &mocks.AWSResource{}
res.On("MetaObject").Return(&metav1.ObjectMeta{
Annotations: map[string]string{
ackv1alpha1.AnnotationAdoptionPolicy: "true",
ackv1alpha1.AnnotationAdopted: "true",
},
})
require.False(ackrt.NeedAdoption(res))

res = &mocks.AWSResource{}
res.On("MetaObject").Return(&metav1.ObjectMeta{
Annotations: map[string]string{
ackv1alpha1.AnnotationAdoptionPolicy: "false",
ackv1alpha1.AnnotationAdopted: "true",
},
})
require.False(ackrt.NeedAdoption(res))
}

func TestExtractAdoptionFields(t *testing.T) {
require := require.New(t)

res := &mocks.AWSResource{}
res.On("MetaObject").Return(&metav1.ObjectMeta{
Annotations: map[string]string{
ackv1alpha1.AnnotationAdoptionFields: `{
"clusterName": "my-cluster",
"name": "ng-1234"
}`,
},
})

expected := map[string]string{
"clusterName": "my-cluster",
"name": "ng-1234",
}
actual, err := ackrt.ExtractAdoptionFields(res)
require.NoError(err)
require.Equal(expected, actual)
}
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 a6b0014

Please sign in to comment.