Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add addoption by annotation feature #164

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
michaelhtm marked this conversation as resolved.
Show resolved Hide resolved
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 :)
Comment on lines +453 to +454
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀


// 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{}
michaelhtm marked this conversation as resolved.
Show resolved Hide resolved
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
michaelhtm marked this conversation as resolved.
Show resolved Hide resolved
}