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

Allow ChildReconciler to opt-in to dangerous chilren #506

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
38 changes: 36 additions & 2 deletions duck/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,19 @@ func NewDuckAwareClientWrapper(client client.Client) client.Client {
}
}

func NewDangerousDuckAwareClientWrapper(client client.Client) client.Client {
return &duckAwareClientWrapper{
Reader: NewDuckAwareAPIReaderWrapper(client, client),
client: client,
allowDangerousRequests: true,
}
}

type duckAwareClientWrapper struct {
client.Reader
client client.Client

allowDangerousRequests bool
}

func (c *duckAwareClientWrapper) Watch(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (watch.Interface, error) {
Expand Down Expand Up @@ -127,15 +137,39 @@ func (c *duckAwareClientWrapper) Create(ctx context.Context, obj client.Object,
return c.client.Create(ctx, obj, opts...)
}

return fmt.Errorf("Create is not supported for the duck typed objects")
if !c.allowDangerousRequests {
return fmt.Errorf("Create is not supported for the duck typed objects")
}

uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return err
}
u := &unstructured.Unstructured{Object: uObj}
if err := c.client.Create(ctx, obj, opts...); err != nil {
return err
}
return runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, obj)
}

func (c *duckAwareClientWrapper) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
if !IsDuck(obj, c.Scheme()) {
return c.client.Update(ctx, obj, opts...)
}

return fmt.Errorf("Update is not supported for the duck typed objects, use Patch instead")
if !c.allowDangerousRequests {
return fmt.Errorf("Update is not supported for the duck typed objects, use Patch instead")
}

uObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return err
}
u := &unstructured.Unstructured{Object: uObj}
if err := c.client.Update(ctx, obj, opts...); err != nil {
return err
}
return runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, obj)
}

func (c *duckAwareClientWrapper) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
Expand Down
33 changes: 31 additions & 2 deletions reconcilers/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ import (
corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"

"reconciler.io/runtime/duck"
"reconciler.io/runtime/internal"
)

Expand Down Expand Up @@ -165,6 +167,16 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL
// +optional
Sanitize func(child ChildType) interface{}

// DangerouslyAllowDuckTypedChildren allows the ChildType to be a duck typed resource. This is
// dangerous because duck types typically represent a subset of the target resource and may
// cause data loss if the resource's server representation contains fields that do not exist on
// the duck typed object.
//
// Use of this setting should be limited to when the author is certain the duck type is able to
// represent the resource with full fidelity, or when data loss for unrepresented fields is
// acceptable.
DangerouslyAllowDuckTypedChildren bool

stamp *ResourceManager[ChildType]
lazyInit sync.Once
}
Expand Down Expand Up @@ -213,10 +225,16 @@ func (r *ChildReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr
return err
}

var ct client.Object = r.ChildType
if duck.IsDuck(ct, mgr.GetScheme()) {
gvk := ct.GetObjectKind().GroupVersionKind()
ct = &unstructured.Unstructured{}
ct.GetObjectKind().SetGroupVersionKind(gvk)
}
if r.SkipOwnerReference {
bldr.Watches(r.ChildType, EnqueueTracked(ctx))
bldr.Watches(ct, EnqueueTracked(ctx))
} else {
bldr.Owns(r.ChildType)
bldr.Owns(ct)
}

if r.Setup == nil {
Expand All @@ -226,6 +244,8 @@ func (r *ChildReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr
}

func (r *ChildReconciler[T, CT, CLT]) validate(ctx context.Context) error {
c := RetrieveConfigOrDie(ctx)

// default implicit values
if r.Finalizer != "" {
r.SkipOwnerReference = true
Expand Down Expand Up @@ -256,6 +276,11 @@ func (r *ChildReconciler[T, CT, CLT]) validate(ctx context.Context) error {
return fmt.Errorf("ChildReconciler %q must implement MergeBeforeUpdate", r.Name)
}

// require DangerouslyAllowDuckTypedChildren for duck types
if !r.DangerouslyAllowDuckTypedChildren && duck.IsDuck(r.ChildType, c.Scheme()) {
return fmt.Errorf("ChildReconciler %q must enable DangerouslyAllowDuckTypedChildren to use a child duck type", r.Name)
}

return nil
}

Expand All @@ -270,6 +295,10 @@ func (r *ChildReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T)
r.init()

c := RetrieveConfigOrDie(ctx)
if r.DangerouslyAllowDuckTypedChildren {
c = c.WithDangerousDuckClientOperations()
ctx = StashConfig(ctx, c)
}

log := logr.FromContextOrDiscard(ctx).
WithName(r.Name).
Expand Down
21 changes: 20 additions & 1 deletion reconcilers/childset.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/go-logr/logr"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"reconciler.io/runtime/duck"
"reconciler.io/runtime/internal"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
Expand Down Expand Up @@ -168,6 +169,16 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje
// +optional
Sanitize func(child ChildType) interface{}

// DangerouslyAllowDuckTypedChildren allows the ChildType to be a duck typed resource. This is
// dangerous because duck types typically represent a subset of the target resource and may
// cause data loss if the resource's server representation contains fields that do not exist on
// the duck typed object.
//
// Use of this setting should be limited to when the author is certain the duck type is able to
// represent the resource with full fidelity, or when data loss for unrepresented fields is
// acceptable.
DangerouslyAllowDuckTypedChildren bool

stamp *ResourceManager[ChildType]
lazyInit sync.Once
voidReconciler *ChildReconciler[Type, ChildType, ChildListType]
Expand Down Expand Up @@ -249,11 +260,14 @@ func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredE
}
return void || id == r.IdentifyChild(child)
},
Sanitize: r.Sanitize,
Sanitize: r.Sanitize,
DangerouslyAllowDuckTypedChildren: r.DangerouslyAllowDuckTypedChildren,
}
}

func (r *ChildSetReconciler[T, CT, CLT]) validate(ctx context.Context) error {
c := RetrieveConfigOrDie(ctx)

// default implicit values
if r.Finalizer != "" {
r.SkipOwnerReference = true
Expand Down Expand Up @@ -284,6 +298,11 @@ func (r *ChildSetReconciler[T, CT, CLT]) validate(ctx context.Context) error {
return fmt.Errorf("ChildSetReconciler %q must implement IdentifyChild", r.Name)
}

// require DangerouslyAllowDuckTypedChildren for duck types
if !r.DangerouslyAllowDuckTypedChildren && duck.IsDuck(r.ChildType, c.Scheme()) {
return fmt.Errorf("ChildSetReconciler %q must enable DangerouslyAllowDuckTypedChildren to use a child duck type", r.Name)
}

return nil
}

Expand Down
15 changes: 15 additions & 0 deletions reconcilers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ func (c Config) WithCluster(cluster cluster.Cluster) Config {
}
}

// WithDangerousDuckClientOperations returns a new Config with client Create and Update methods for
// duck typed objects enabled.
//
// This is dangerous because duck types typically represent a subset of the target resource and may
// cause data loss if the resource's server representation contains fields that do not exist on the
// duck typed object.
func (c Config) WithDangerousDuckClientOperations() Config {
return Config{
Client: duck.NewDangerousDuckAwareClientWrapper(c.Client),
APIReader: c.APIReader,
Recorder: c.Recorder,
Tracker: c.Tracker,
}
}

// TrackAndGet tracks the resources for changes and returns the current value. The track is
// registered even when the resource does not exists so that its creation can be tracked.
//
Expand Down
Loading
Loading