Skip to content

Commit a6b0014

Browse files
authored
Merge pull request #164 from michaelhtm/force-adoption
Add addoption by annotation feature
2 parents b5e7fbe + 4d78380 commit a6b0014

File tree

7 files changed

+223
-0
lines changed

7 files changed

+223
-0
lines changed

apis/core/v1alpha1/annotations.go

+10
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,14 @@ const (
8181
// the resource is read-only and should not be created/patched/deleted by the
8282
// ACK service controller.
8383
AnnotationReadOnly = AnnotationPrefix + "read-only"
84+
// AnnotationAdoptionPolicy is an annotation whose value is the identifier for whether
85+
// we will attempt adoption only (value = adopt-only) or attempt a create if resource
86+
// is not found (value adopt-or-create).
87+
//
88+
// NOTE (michaelhtm): Currently create-or-adopt is not supported
89+
AnnotationAdoptionPolicy = AnnotationPrefix + "adoption-policy"
90+
// AnnotationAdoptionFields is an annotation whose value contains a json-like
91+
// format of the requied fields to do a ReadOne when attempting to force-adopt
92+
// a Resource
93+
AnnotationAdoptionFields = AnnotationPrefix + "adoption-fields"
8494
)

mocks/pkg/types/aws_resource.go

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/featuregate/features.go

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ package featuregate
1919
import "fmt"
2020

2121
const (
22+
// ResourceAdoption is a feature gate for enabling forced adoption of resources
23+
// by annotation
24+
ResourceAdoption = "ResourceAdoption"
25+
2226
// ReadOnlyResources is a feature gate for enabling ReadOnly resources annotation.
2327
ReadOnlyResources = "ReadOnlyResources"
2428

@@ -32,6 +36,7 @@ const (
3236
// defaultACKFeatureGates is a map of feature names to Feature structs
3337
// representing the default feature gates for ACK controllers.
3438
var defaultACKFeatureGates = FeatureGates{
39+
ResourceAdoption: {Stage: Alpha, Enabled: false},
3540
ReadOnlyResources: {Stage: Alpha, Enabled: false},
3641
TeamLevelCARM: {Stage: Alpha, Enabled: false},
3742
ServiceLevelCARM: {Stage: Alpha, Enabled: false},

pkg/runtime/reconciler.go

+85
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const (
5757
// resource if the CARM cache is not synced yet, or if the roleARN is not
5858
// available.
5959
roleARNNotAvailableRequeueDelay = 15 * time.Second
60+
// adoptOrCreate is an annotation field that decides whether to create the
61+
// resource if it doesn't exist, or adopt the resource if it exists.
62+
// value comes from getAdoptionPolicy
63+
// adoptOrCreate = "adopt-or-create"
6064
)
6165

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

305+
func (r *resourceReconciler) handleAdoption(
306+
ctx context.Context,
307+
rm acktypes.AWSResourceManager,
308+
desired acktypes.AWSResource,
309+
rlog acktypes.Logger,
310+
) (acktypes.AWSResource, error) {
311+
// If the resource is being adopted by force, we need to access
312+
// the required field passed by annotation and attempt a read.
313+
314+
rlog.Info("Adopting Resource")
315+
extractedFields, err := ExtractAdoptionFields(desired)
316+
if err != nil {
317+
return desired, ackerr.NewTerminalError(err)
318+
}
319+
if len(extractedFields) == 0 {
320+
// TODO(michaelhtm) Here we need to figure out if we want to have an
321+
// error or not. should we consider accepting values from Spec?
322+
// And then we can let the ReadOne figure out if we have missing
323+
// required fields for a Read
324+
return nil, fmt.Errorf("failed extracting fields from annotation")
325+
}
326+
resolved := desired.DeepCopy()
327+
err = resolved.PopulateResourceFromAnnotation(extractedFields)
328+
if err != nil {
329+
return nil, err
330+
}
331+
332+
rlog.Enter("rm.EnsureTags")
333+
err = rm.EnsureTags(ctx, resolved, r.sc.GetMetadata())
334+
rlog.Exit("rm.EnsureTags", err)
335+
if err != nil {
336+
return resolved, err
337+
}
338+
rlog.Enter("rm.ReadOne")
339+
latest, err := rm.ReadOne(ctx, resolved)
340+
if err != nil {
341+
return latest, err
342+
}
343+
344+
if err = r.setResourceManaged(ctx, rm, latest); err != nil {
345+
return latest, err
346+
}
347+
348+
// Ensure tags again after adding the finalizer and patching the
349+
// resource. Patching desired resource omits the controller tags
350+
// because they are not persisted in etcd. So we again ensure
351+
// that tags are present before performing the create operation.
352+
rlog.Enter("rm.EnsureTags")
353+
err = rm.EnsureTags(ctx, latest, r.sc.GetMetadata())
354+
rlog.Exit("rm.EnsureTags", err)
355+
if err != nil {
356+
return latest, err
357+
}
358+
r.rd.MarkAdopted(latest)
359+
rlog.WithValues("is_adopted", "true")
360+
latest, err = r.patchResourceMetadataAndSpec(ctx, rm, desired, latest)
361+
if err != nil {
362+
return latest, err
363+
}
364+
365+
rlog.Info("Resource Adopted")
366+
return latest, nil
367+
}
368+
301369
// reconcile either cleans up a deleted resource or ensures that the supplied
302370
// AWSResource's backing API resource matches the supplied desired state.
303371
//
@@ -360,13 +428,30 @@ func (r *resourceReconciler) Sync(
360428
isAdopted := IsAdopted(desired)
361429
rlog.WithValues("is_adopted", isAdopted)
362430

431+
if r.cfg.FeatureGates.IsEnabled(featuregate.ResourceAdoption) {
432+
if NeedAdoption(desired) && !r.rd.IsManaged(desired) {
433+
latest, err := r.handleAdoption(ctx, rm, desired, rlog)
434+
435+
if err != nil {
436+
// If we get an error, we want to return here
437+
// TODO(michaelhtm): Change the handling of
438+
// the error to allow Adopt or Create here
439+
// when supported
440+
return latest, err
441+
}
442+
return latest, nil
443+
}
444+
}
445+
363446
if r.cfg.FeatureGates.IsEnabled(featuregate.ReadOnlyResources) {
364447
isReadOnly := IsReadOnly(desired)
365448
rlog.WithValues("is_read_only", isReadOnly)
366449

367450
// NOTE(a-hilaly): When the time comes to support adopting resources
368451
// using annotations, we will need to think a little bit more about
369452
// the case where a user, wants to adopt a resource as read-only.
453+
//
454+
// NOTE(michaelhtm): Done, tnx :)
370455

371456
// If the resource is read-only, we enter a different code path where we
372457
// only read the resource and patch the metadata and spec.

pkg/runtime/util.go

+53
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package runtime
1515

1616
import (
17+
"encoding/json"
1718
"strings"
1819

1920
corev1 "k8s.io/api/core/v1"
@@ -68,3 +69,55 @@ func IsReadOnly(res acktypes.AWSResource) bool {
6869
}
6970
return false
7071
}
72+
73+
// GetAdoptionPolicy returns the Adoption Policy of the resource
74+
// defined by the user in annotation. Possible values are:
75+
// adopt-only | adopt-or-create
76+
// adopt-only keeps requing until the resource is found
77+
// adopt-or-create creates the resource if does not exist
78+
func GetAdoptionPolicy(res acktypes.AWSResource) string {
79+
mo := res.MetaObject()
80+
if mo == nil {
81+
panic("getAdoptionPolicy received resource with nil RuntimeObject")
82+
}
83+
for k, v := range mo.GetAnnotations() {
84+
if k == ackv1alpha1.AnnotationAdoptionPolicy {
85+
return v
86+
}
87+
}
88+
89+
return ""
90+
}
91+
92+
// NeedAdoption returns true when the resource has
93+
// adopt annotation but is not yet adopted
94+
func NeedAdoption(res acktypes.AWSResource) bool {
95+
return GetAdoptionPolicy(res) != "" && !IsAdopted(res)
96+
}
97+
98+
func ExtractAdoptionFields(res acktypes.AWSResource) (map[string]string, error) {
99+
fields := getAdoptionFields(res)
100+
101+
extractedFields := &map[string]string{}
102+
err := json.Unmarshal([]byte(fields), extractedFields)
103+
if err != nil {
104+
return nil, err
105+
}
106+
107+
return *extractedFields, nil
108+
}
109+
110+
func getAdoptionFields(res acktypes.AWSResource) string {
111+
mo := res.MetaObject()
112+
if mo == nil {
113+
// Should never happen... if it does, it's buggy code.
114+
panic("ExtractRequiredFields received resource with nil RuntimeObject")
115+
}
116+
117+
for k, v := range mo.GetAnnotations() {
118+
if k == ackv1alpha1.AnnotationAdoptionFields {
119+
return v
120+
}
121+
}
122+
return ""
123+
}

pkg/runtime/util_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,56 @@ func TestIsSynced(t *testing.T) {
6767
})
6868
require.False(ackrt.IsSynced(res))
6969
}
70+
71+
func TestIsForcedAdoption(t *testing.T) {
72+
require := require.New(t)
73+
74+
res := &mocks.AWSResource{}
75+
res.On("MetaObject").Return(&metav1.ObjectMeta{
76+
Annotations: map[string]string{
77+
ackv1alpha1.AnnotationAdoptionPolicy: "true",
78+
ackv1alpha1.AnnotationAdopted: "false",
79+
},
80+
})
81+
require.True(ackrt.NeedAdoption(res))
82+
83+
res = &mocks.AWSResource{}
84+
res.On("MetaObject").Return(&metav1.ObjectMeta{
85+
Annotations: map[string]string{
86+
ackv1alpha1.AnnotationAdoptionPolicy: "true",
87+
ackv1alpha1.AnnotationAdopted: "true",
88+
},
89+
})
90+
require.False(ackrt.NeedAdoption(res))
91+
92+
res = &mocks.AWSResource{}
93+
res.On("MetaObject").Return(&metav1.ObjectMeta{
94+
Annotations: map[string]string{
95+
ackv1alpha1.AnnotationAdoptionPolicy: "false",
96+
ackv1alpha1.AnnotationAdopted: "true",
97+
},
98+
})
99+
require.False(ackrt.NeedAdoption(res))
100+
}
101+
102+
func TestExtractAdoptionFields(t *testing.T) {
103+
require := require.New(t)
104+
105+
res := &mocks.AWSResource{}
106+
res.On("MetaObject").Return(&metav1.ObjectMeta{
107+
Annotations: map[string]string{
108+
ackv1alpha1.AnnotationAdoptionFields: `{
109+
"clusterName": "my-cluster",
110+
"name": "ng-1234"
111+
}`,
112+
},
113+
})
114+
115+
expected := map[string]string{
116+
"clusterName": "my-cluster",
117+
"name": "ng-1234",
118+
}
119+
actual, err := ackrt.ExtractAdoptionFields(res)
120+
require.NoError(err)
121+
require.Equal(expected, actual)
122+
}

pkg/types/aws_resource.go

+3
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,7 @@ type AWSResource interface {
4848
SetStatus(AWSResource)
4949
// DeepCopy will return a copy of the resource
5050
DeepCopy() AWSResource
51+
// PopulateResourceFromAnnotation will set the Spec or Status field that user
52+
// provided from annotations
53+
PopulateResourceFromAnnotation(fields map[string]string) error
5154
}

0 commit comments

Comments
 (0)