Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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"
Comment thread
michaelhtm marked this conversation as resolved.
Outdated

// 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
77 changes: 77 additions & 0 deletions pkg/runtime/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,13 +360,90 @@ func (r *resourceReconciler) Sync(
isAdopted := IsAdopted(desired)
rlog.WithValues("is_adopted", isAdopted)

if r.cfg.FeatureGates.IsEnabled(featuregate.ForcedAdoptResources) {

// If the resource is being adopted by force, we need to access
// the required field passed by annotation and attempt a read.
if NeedAdoption(desired) {
rlog.WithValues("is_forced_adoption", "true")
Comment thread
michaelhtm marked this conversation as resolved.
Outdated
rlog.Info("Adopting Resource")
extractedFields, err := ExtractAdoptionFields(desired)
Comment thread
michaelhtm marked this conversation as resolved.
Outdated
if err != nil {
return desired, err
}
if extractedFields == nil {
// TODO(michaelhtm) figure out error here
return nil, fmt.Errorf("Failed extracting fields from annotation")
}
Comment thread
michaelhtm marked this conversation as resolved.
Outdated
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)
}
}
Comment thread
michaelhtm marked this conversation as resolved.
Outdated

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, requeue.NeededAfter(nil, 5)
}
}

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
Copy Markdown
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
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 supplied AWSResource has an annotation
// indicating that it should be adopted
func isForcedAdoption(res acktypes.AWSResource) bool {
Comment thread
michaelhtm marked this conversation as resolved.
Outdated
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) {
Comment thread
michaelhtm marked this conversation as resolved.
fields := extractFieldsFromAnnotation(res)

extractedFields := &map[string]string{}
Comment thread
michaelhtm marked this conversation as resolved.
err := json.Unmarshal([]byte(fields), extractedFields)
if err != nil {
return nil, err
}

return *extractedFields, nil
}

func extractFieldsFromAnnotation(res acktypes.AWSResource) string {
Comment thread
michaelhtm marked this conversation as resolved.
Outdated
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.AnnotationForceAdoption: "true",
ackv1alpha1.AnnotationAdopted: "false",
},
})
require.True(ackrt.NeedAdoption(res))

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

res = &mocks.AWSResource{}
res.On("MetaObject").Return(&metav1.ObjectMeta{
Annotations: map[string]string{
ackv1alpha1.AnnotationForceAdoption: "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
Comment thread
michaelhtm marked this conversation as resolved.
}