diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 0f9fed32..2c4eda7c 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -18,10 +18,85 @@ package v1alpha1 import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) +// ObjectAction defines actions applicable to Object +type ObjectAction string + +// A ManagementPolicy determines what should happen to the underlying external +// resource when a managed resource is created, updated, deleted, or observed. +// +kubebuilder:validation:Enum=Default;ObserveCreateUpdate;ObserveDelete;Observe +type ManagementPolicy string + +const ( + // Default means the provider can fully manage the resource. + Default ManagementPolicy = "Default" + // ObserveCreateUpdate means the provider can observe, create, or update + // the resource, but can not delete it. + ObserveCreateUpdate ManagementPolicy = "ObserveCreateUpdate" + // ObserveDelete means the provider can observe or delete the resource, but + // can not create and update it. + ObserveDelete ManagementPolicy = "ObserveDelete" + // Observe means the provider can only observe the resource. + Observe ManagementPolicy = "Observe" + + // ObjectActionCreate means to create an Object + ObjectActionCreate ObjectAction = "Create" + // ObjectActionUpdate means to update an Object + ObjectActionUpdate ObjectAction = "Update" + // ObjectActionDelete means to delete an Object + ObjectActionDelete ObjectAction = "Delete" +) + +// DependsOn refers to an object by Name, Kind, APIVersion, etc. It is used to +// reference other Object or arbitrary Kubernetes resource which is either +// cluster or namespace scoped. +type DependsOn struct { + // APIVersion of the referenced object. + // +kubebuilder:default=kubernetes.crossplane.io/v1alpha1 + // +optional + APIVersion string `json:"apiVersion,omitempty"` + // Kind of the referenced object. + // +kubebuilder:default=Object + // +optional + Kind string `json:"kind,omitempty"` + // Name of the referenced object. + Name string `json:"name"` + // Namespace of the referenced object. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// PatchesFrom refers to an object by Name, Kind, APIVersion, etc., and patch +// fields from this object. +type PatchesFrom struct { + DependsOn `json:",inline"` + // FieldPath is the path of the field on the resource whose value is to be + // used as input. + FieldPath *string `json:"fieldPath"` +} + +// Reference refers to an Object or arbitrary Kubernetes resource and optionally +// patch values from that resource to the current Object. +type Reference struct { + // DependsOn is used to declare dependency on other Object or arbitrary + // Kubernetes resource. + // +optional + *DependsOn `json:"dependsOn,omitempty"` + // PatchesFrom is used to declare dependency on other Object or arbitrary + // Kubernetes resource, and also patch fields from this object. + // +optional + *PatchesFrom `json:"patchesFrom,omitempty"` + // ToFieldPath is the path of the field on the resource whose value will + // be changed with the result of transforms. Leave empty if you'd like to + // propagate to the same path as patchesFrom.fieldPath. + // +optional + ToFieldPath *string `json:"toFieldPath,omitempty"` +} + // ObjectParameters are the configurable fields of a Object. type ObjectParameters struct { // Raw JSON representation of the kubernetes object to be created. @@ -41,7 +116,10 @@ type ObjectObservation struct { // A ObjectSpec defines the desired state of a Object. type ObjectSpec struct { xpv1.ResourceSpec `json:",inline"` - ForProvider ObjectParameters `json:"forProvider"` + // +kubebuilder:default=Default + ManagementPolicy `json:"managementPolicy,omitempty"` + References []Reference `json:"references,omitempty"` + ForProvider ObjectParameters `json:"forProvider"` } // A ObjectStatus represents the observed state of a Object. @@ -74,3 +152,51 @@ type ObjectList struct { metav1.ListMeta `json:"metadata,omitempty"` Items []Object `json:"items"` } + +// ApplyFromFieldPathPatch patches the "to" resource, using a source field +// on the "from" resource. +func (r *Reference) ApplyFromFieldPathPatch(from, to runtime.Object) error { + // Default to patch the same field on the "to" resource. + if r.ToFieldPath == nil { + r.ToFieldPath = r.PatchesFrom.FieldPath + } + + paved, err := fieldpath.PaveObject(from) + if err != nil { + return err + } + + out, err := paved.GetValue(*r.PatchesFrom.FieldPath) + if err != nil { + return err + } + + return patchFieldValueToObject(*r.ToFieldPath, out, to) +} + +// patchFieldValueToObject, given a path, value and "to" object, will +// apply the value to the "to" object at the given path, returning +// any errors as they occur. +func patchFieldValueToObject(path string, value interface{}, to runtime.Object) error { + paved, err := fieldpath.PaveObject(to) + if err != nil { + return err + } + + err = paved.SetValue("spec.forProvider.manifest."+path, value) + if err != nil { + return err + } + + return runtime.DefaultUnstructuredConverter.FromUnstructured(paved.UnstructuredContent(), to) +} + +// IsActionAllowed determines if action is allowed to be performed on Object +func (p *ManagementPolicy) IsActionAllowed(action ObjectAction) bool { + if action == ObjectActionCreate || action == ObjectActionUpdate { + return *p == Default || *p == ObserveCreateUpdate + } + + // ObjectActionDelete + return *p == Default || *p == ObserveDelete +} diff --git a/apis/object/v1alpha1/zz_generated.deepcopy.go b/apis/object/v1alpha1/zz_generated.deepcopy.go index 84fe4096..0fb6f2f7 100644 --- a/apis/object/v1alpha1/zz_generated.deepcopy.go +++ b/apis/object/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,21 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DependsOn) DeepCopyInto(out *DependsOn) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependsOn. +func (in *DependsOn) DeepCopy() *DependsOn { + if in == nil { + return nil + } + out := new(DependsOn) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Object) DeepCopyInto(out *Object) { *out = *in @@ -119,6 +134,13 @@ func (in *ObjectParameters) DeepCopy() *ObjectParameters { func (in *ObjectSpec) DeepCopyInto(out *ObjectSpec) { *out = *in in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + if in.References != nil { + in, out := &in.References, &out.References + *out = make([]Reference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.ForProvider.DeepCopyInto(&out.ForProvider) } @@ -148,3 +170,54 @@ func (in *ObjectStatus) DeepCopy() *ObjectStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PatchesFrom) DeepCopyInto(out *PatchesFrom) { + *out = *in + out.DependsOn = in.DependsOn + if in.FieldPath != nil { + in, out := &in.FieldPath, &out.FieldPath + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchesFrom. +func (in *PatchesFrom) DeepCopy() *PatchesFrom { + if in == nil { + return nil + } + out := new(PatchesFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Reference) DeepCopyInto(out *Reference) { + *out = *in + if in.DependsOn != nil { + in, out := &in.DependsOn, &out.DependsOn + *out = new(DependsOn) + **out = **in + } + if in.PatchesFrom != nil { + in, out := &in.PatchesFrom, &out.PatchesFrom + *out = new(PatchesFrom) + (*in).DeepCopyInto(*out) + } + if in.ToFieldPath != nil { + in, out := &in.ToFieldPath, &out.ToFieldPath + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Reference. +func (in *Reference) DeepCopy() *Reference { + if in == nil { + return nil + } + out := new(Reference) + in.DeepCopyInto(out) + return out +} diff --git a/examples/object/policy/default.yaml b/examples/object/policy/default.yaml new file mode 100644 index 00000000..f19d7edb --- /dev/null +++ b/examples/object/policy/default.yaml @@ -0,0 +1,17 @@ +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy Default to fully control k8s resource + # It is the default policy that can be omitted + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/policy/observe-create-update.yaml b/examples/object/policy/observe-create-update.yaml new file mode 100644 index 00000000..87f83805 --- /dev/null +++ b/examples/object/policy/observe-create-update.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy ObserveCreateUpdate to observe, create, or update k8s + # resource, but leave to third party to delete the resource + managementPolicy: ObserveCreateUpdate + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/policy/observe-delete.yaml b/examples/object/policy/observe-delete.yaml new file mode 100644 index 00000000..89cd1efe --- /dev/null +++ b/examples/object/policy/observe-delete.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + sample-key: sample-value +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy ObserveDelete to observe or delete k8s resource, + # but leave to third party to create or update the resource + managementPolicy: ObserveDelete + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/policy/observe.yaml b/examples/object/policy/observe.yaml new file mode 100644 index 00000000..1df0c1d6 --- /dev/null +++ b/examples/object/policy/observe.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + sample-key: sample-value +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + # Use management policy Observe to observe k8s resource, + # but leave to third party to create, update, or delete the resource + managementPolicy: Observe + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + # name in manifest is optional and defaults to Object name + # name: some-other-name + namespace: default + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/references/depends-on-object.yaml b/examples/object/references/depends-on-object.yaml new file mode 100644 index 00000000..5c70651c --- /dev/null +++ b/examples/object/references/depends-on-object.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + references: + # Use dependsOn to declare dependency on other object for this object + - dependsOn: + # apiVersion is optional and defaults to Object apiVersion + # kind is optional and defaults to Object kind + # namespace is not needed when it is cluster-scoped resource + name: bar + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: bar +spec: + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider diff --git a/examples/object/references/depends-on-resource.yaml b/examples/object/references/depends-on-resource.yaml new file mode 100644 index 00000000..84673fe9 --- /dev/null +++ b/examples/object/references/depends-on-resource.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + references: + # Use dependsOn to declare dependency on other k8s resource for this object + - dependsOn: + apiVersion: v1 + kind: ConfigMap + name: bar + namespace: default + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bar + namespace: default +data: + sample-key: sample-value diff --git a/examples/object/references/patches-from-multiple-resources.yaml b/examples/object/references/patches-from-multiple-resources.yaml new file mode 100644 index 00000000..e7490429 --- /dev/null +++ b/examples/object/references/patches-from-multiple-resources.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + references: + # Use multiple patchesFrom to patch fields from multiple k8s resources + - patchesFrom: + apiVersion: v1 + kind: ConfigMap + name: bar + namespace: default + fieldPath: data.sample-key-from-bar + # toFieldPath in manifest is optional and defaults to fieldPath + # toFieldPath: data.sample-key-from-bar + - patchesFrom: + apiVersion: v1 + kind: ConfigMap + name: baz + namespace: default + fieldPath: data.sample-key-from-baz + # toFieldPath in manifest is optional and defaults to fieldPath + # toFieldPath: data.sample-key-from-baz + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bar + namespace: default +data: + sample-key-from-bar: sample-value +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: baz + namespace: default +data: + sample-key-from-baz: sample-value diff --git a/examples/object/references/patches-from-resource.yaml b/examples/object/references/patches-from-resource.yaml new file mode 100644 index 00000000..c2821c6c --- /dev/null +++ b/examples/object/references/patches-from-resource.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: Object +metadata: + name: foo +spec: + references: + # Use patchesFrom to patch field from other k8s resource to this object + - patchesFrom: + apiVersion: v1 + kind: ConfigMap + name: bar + namespace: default + fieldPath: data.sample-key + toFieldPath: data.sample-key-from-bar + forProvider: + manifest: + apiVersion: v1 + kind: ConfigMap + metadata: + namespace: default + data: + sample-key: sample-value + providerConfigRef: + name: kubernetes-provider +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bar + namespace: default +data: + sample-key: sample-value diff --git a/go.mod b/go.mod index 4c7786ce..1ee7448e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( k8s.io/api v0.20.1 k8s.io/apimachinery v0.20.1 k8s.io/client-go v0.20.1 + k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/controller-runtime v0.8.0 sigs.k8s.io/controller-tools v0.3.0 ) diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 662bf575..e3a5a869 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -65,6 +65,17 @@ const ( errGetLastApplied = "cannot get last applied" errUnmarshalTemplate = "cannot unmarshal template" errFailedToMarshalExisting = "cannot marshal existing resource" + + errGetReferencedResource = "cannot get referenced resource" + errPatchFromReferencedResource = "cannot patch from referenced resource" + errResolveResourceReferences = "cannot resolve resource references" + + errAddFinalizer = "cannot add finalizer to Object" + errRemoveFinalizer = "cannot remove finalizer from Object" + errAddReferenceFinalizer = "cannot add finalizer to referenced resource" + errRemoveReferenceFinalizer = "cannot remove finalizer from referenced resource" + objFinalizerName = "finalizer.managedresource.crossplane.io" + refFinalizerNamePrefix = "kubernetes.crossplane.io/referred-by-object-" ) // Setup adds a controller that reconciles Object managed resources. @@ -89,6 +100,7 @@ func Setup(mgr ctrl.Manager, l logging.Logger, rl workqueue.RateLimiter, poll ti newRESTConfigFn: clients.NewRESTConfig, newKubeClientFn: clients.NewKubeClient, }), + managed.WithFinalizer(&objFinalizer{client: mgr.GetClient()}), managed.WithLogger(logger), managed.WithPollInterval(poll), managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) @@ -175,12 +187,15 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E Client: k, Applicator: resource.NewAPIPatchingApplicator(k), }, + localClient: c.kube, }, nil } type external struct { logger logging.Logger client resource.ClientApplicator + // localClient is specifically used to connect to local cluster + localClient client.Client } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { @@ -191,6 +206,10 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex c.logger.Debug("Observing", "resource", cr) + if err := c.resolveReferencies(ctx, cr); err != nil { + return managed.ExternalObservation{}, errors.Wrap(err, errResolveResourceReferences) + } + desired, err := getDesired(cr) if err != nil { return managed.ExternalObservation{}, err @@ -203,9 +222,10 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex Name: observed.GetName(), }, observed) - if kerrors.IsNotFound(err) { + if c.isNotFound(cr, err) { return managed.ExternalObservation{ResourceExists: false}, nil } + if err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errGetObject) } @@ -218,25 +238,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex if last, err = getLastApplied(cr, observed); err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errGetLastApplied) } - if last == nil { - return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: false, - }, nil - } - - if equality.Semantic.DeepEqual(last, desired) { - c.logger.Debug("Up to date!") - return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: true, - }, nil - } - - return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: false, - }, nil + return c.handleLastApplied(cr, last, desired) } func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { @@ -246,6 +248,12 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } c.logger.Debug("Creating", "resource", cr) + + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionCreate) { + c.logger.Debug("External resource should not be created by provider, skip creating.") + return managed.ExternalCreation{}, nil + } + obj, err := getDesired(cr) if err != nil { return managed.ExternalCreation{}, err @@ -259,7 +267,6 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.Wrap(err, errCreateObject) } - cr.Status.SetConditions(xpv1.Available()) return managed.ExternalCreation{}, setObserved(cr, obj) } @@ -271,6 +278,11 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Updating", "resource", cr) + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionUpdate) { + c.logger.Debug("External resource should not be updated by provider, skip updating.") + return managed.ExternalUpdate{}, nil + } + obj, err := getDesired(cr) if err != nil { return managed.ExternalUpdate{}, err @@ -284,7 +296,6 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, errors.Wrap(err, errApplyObject) } - cr.Status.SetConditions(xpv1.Available()) return managed.ExternalUpdate{}, setObserved(cr, obj) } @@ -295,6 +306,12 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { } c.logger.Debug("Deleting", "resource", cr) + + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { + c.logger.Debug("External resource should not be deleted by provider, skip deleting.") + return nil + } + obj, err := getDesired(cr) if err != nil { return err @@ -340,3 +357,207 @@ func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) erro } return nil } + +func getReferenceInfo(ref v1alpha1.Reference) (string, string, string, string) { + var apiVersion, kind, namespace, name string + + if ref.PatchesFrom != nil { + // Reference information defined in PatchesFrom + apiVersion = ref.PatchesFrom.APIVersion + kind = ref.PatchesFrom.Kind + namespace = ref.PatchesFrom.Namespace + name = ref.PatchesFrom.Name + } else if ref.DependsOn != nil { + // Reference information defined in DependsOn + apiVersion = ref.DependsOn.APIVersion + kind = ref.DependsOn.Kind + namespace = ref.DependsOn.Namespace + name = ref.DependsOn.Name + } + + return apiVersion, kind, namespace, name +} + +// resolveReferencies resolves references for the current Object. If it fails to +// resolve some reference, e.g.: due to reference not ready, it will then return +// error and requeue to wait for resolving it next time. +func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) error { + c.logger.Debug("Resolving referencies.") + + // Loop through references to resolve each referenced resource + for _, ref := range obj.Spec.References { + if ref.DependsOn == nil && ref.PatchesFrom == nil { + continue + } + + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(ref) + res := &unstructured.Unstructured{} + res.SetAPIVersion(refAPIVersion) + res.SetKind(refKind) + // Try to get referenced resource + err := c.localClient.Get(ctx, client.ObjectKey{ + Namespace: refNamespace, + Name: refName, + }, res) + + if err != nil { + return errors.Wrap(err, errGetReferencedResource) + } + + // Patch fields if any + if ref.PatchesFrom != nil && ref.PatchesFrom.FieldPath != nil { + if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { + return errors.Wrap(err, errPatchFromReferencedResource) + } + } + } + + return nil +} + +func (c *external) isNotFound(obj *v1alpha1.Object, err error) bool { + isNotFound := false + + if kerrors.IsNotFound(err) { + isNotFound = true + } else if meta.WasDeleted(obj) { + // If the Object resource was being deleted but the external resource is + // not deletable as management policy is specified, we should return the + // external resource not found, so that Object can be deleted by managed + // resource reconciler. Otherwise, the reconciler will try to delete the + // external resource which breaks the management policy. + if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { + c.logger.Debug("Managed resource was deleted but external resource is undeletable.") + isNotFound = true + } + } + + return isNotFound +} + +func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { + isUpToDate := false + + if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionUpdate) { + // Treated as up-to-date to skip last applied annotation update since we + // do not create or update the external resource. + isUpToDate = true + } else if last != nil && equality.Semantic.DeepEqual(last, desired) { + // Mark as up-to-date since last is equal to desired + isUpToDate = true + } + + if isUpToDate { + c.logger.Debug("Up to date!") + + obj.Status.SetConditions(xpv1.Available()) + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, nil + } + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + }, nil +} + +type objFinalizer struct { + resource.Finalizer + client client.Client +} + +type refFinalizerFn func(context.Context, *unstructured.Unstructured, string) error + +func (f *objFinalizer) handleRefFinalizer(ctx context.Context, obj *v1alpha1.Object, finalizerFn refFinalizerFn) error { + // Loop through references to resolve each referenced resource + for _, ref := range obj.Spec.References { + if ref.DependsOn == nil && ref.PatchesFrom == nil { + continue + } + + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(ref) + res := &unstructured.Unstructured{} + res.SetAPIVersion(refAPIVersion) + res.SetKind(refKind) + // Try to get referenced resource + err := f.client.Get(ctx, client.ObjectKey{ + Namespace: refNamespace, + Name: refName, + }, res) + + if err != nil { + return errors.Wrap(err, errGetReferencedResource) + } + + finalizerName := refFinalizerNamePrefix + string(obj.UID) + if err = finalizerFn(ctx, res, finalizerName); err != nil { + return err + } + } + + return nil + +} + +func (f *objFinalizer) AddFinalizer(ctx context.Context, res resource.Object) error { + obj, ok := res.(*v1alpha1.Object) + if !ok { + return errors.New(errNotKubernetesObject) + } + + if meta.FinalizerExists(obj, objFinalizerName) { + return nil + } + meta.AddFinalizer(obj, objFinalizerName) + + err := f.client.Update(ctx, obj) + if err != nil { + return errors.Wrap(err, errAddFinalizer) + } + + // Add finalizer to referenced resources if not exists + err = f.handleRefFinalizer(ctx, obj, func( + ctx context.Context, res *unstructured.Unstructured, finalizer string) error { + if !meta.FinalizerExists(res, finalizer) { + meta.AddFinalizer(res, finalizer) + if err := f.client.Update(ctx, res); err != nil { + return errors.Wrap(err, errAddReferenceFinalizer) + } + } + return nil + }) + return errors.Wrap(err, errAddFinalizer) +} + +func (f *objFinalizer) RemoveFinalizer(ctx context.Context, res resource.Object) error { + obj, ok := res.(*v1alpha1.Object) + if !ok { + return errors.New(errNotKubernetesObject) + } + + if !meta.FinalizerExists(obj, objFinalizerName) { + return nil + } + meta.RemoveFinalizer(obj, objFinalizerName) + + err := f.client.Update(ctx, obj) + if err != nil { + return errors.Wrap(err, errRemoveFinalizer) + } + + // Remove finalizer from referenced resources if exists + err = f.handleRefFinalizer(ctx, obj, func( + ctx context.Context, res *unstructured.Unstructured, finalizer string) error { + if meta.FinalizerExists(res, finalizer) { + meta.RemoveFinalizer(res, finalizer) + if err := f.client.Update(ctx, res); err != nil { + return errors.Wrap(err, errRemoveReferenceFinalizer) + } + } + return nil + }) + return errors.Wrap(err, errRemoveFinalizer) +} diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 6f98de35..12b6eb86 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -19,6 +19,7 @@ package object import ( "context" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" @@ -29,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" @@ -49,8 +51,9 @@ const ( providerSecretKey = "kubeconfig" providerSecretData = "somethingsecret" - testObjectName = "test-object" - testNamespace = "test-namespace" + testObjectName = "test-object" + testNamespace = "test-namespace" + testReferenceObjectName = "test-ref-object" ) var ( @@ -62,14 +65,20 @@ type notKubernetesObject struct { } type kubernetesObjectModifier func(obj *v1alpha1.Object) +type externalResourceModifier func(res *unstructured.Unstructured) func kubernetesObject(om ...kubernetesObjectModifier) *v1alpha1.Object { o := &v1alpha1.Object{ + TypeMeta: metav1.TypeMeta{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: v1alpha1.ObjectKind, + }, ObjectMeta: metav1.ObjectMeta{ Name: testObjectName, Namespace: testNamespace, }, Spec: v1alpha1.ObjectSpec{ + ManagementPolicy: v1alpha1.Default, ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{ Name: providerName, @@ -95,6 +104,95 @@ func kubernetesObject(om ...kubernetesObjectModifier) *v1alpha1.Object { return o } +func externalResource(rm ...externalResourceModifier) *unstructured.Unstructured { + res := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "crossplane-system", + }, + }, + } + + for _, m := range rm { + m(res) + } + + return res +} + +func externalResourceWithLastAppliedConfigAnnotation(val interface{}) *unstructured.Unstructured { + res := externalResource(func(res *unstructured.Unstructured) { + metadata := res.Object["metadata"].(map[string]interface{}) + metadata["annotations"] = map[string]interface{}{ + corev1.LastAppliedConfigAnnotation: val, + } + }) + return res +} + +func objectReferences() []v1alpha1.Reference { + dependsOn := v1alpha1.DependsOn{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: v1alpha1.ObjectKind, + Name: testReferenceObjectName, + Namespace: testNamespace, + } + ref := []v1alpha1.Reference{ + { + PatchesFrom: &v1alpha1.PatchesFrom{ + DependsOn: dependsOn, + }, + }, + { + DependsOn: &dependsOn, + }, + } + return ref +} + +func referenceObject(rm ...externalResourceModifier) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": v1alpha1.SchemeGroupVersion.String(), + "kind": v1alpha1.ObjectKind, + "metadata": map[string]interface{}{ + "name": testReferenceObjectName, + "namespace": testNamespace, + }, + "spec": map[string]interface{}{ + "forProvider": map[string]interface{}{ + "manifest": map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "namespace": testNamespace, + "labels": map[string]interface{}{ + "app": "foo", + }, + }, + }, + }, + }, + }, + } + + for _, m := range rm { + m(obj) + } + + return obj +} + +func referenceObjectWithFinalizer(val interface{}) *unstructured.Unstructured { + res := referenceObject(func(res *unstructured.Unstructured) { + metadata := res.Object["metadata"].(map[string]interface{}) + metadata["finalizers"] = []interface{}{val} + }) + return res +} + type providerConfigModifier func(pc *kubernetesv1alpha1.ProviderConfig) func providerConfig(pm ...providerConfigModifier) *kubernetesv1alpha1.ProviderConfig { @@ -457,15 +555,7 @@ func Test_helmExternal_Observe(t *testing.T) { client: resource.ClientApplicator{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { - *obj.(*unstructured.Unstructured) = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "crossplane-system", - }, - }, - } + *obj.(*unstructured.Unstructured) = *externalResource() return nil }), }, @@ -482,18 +572,10 @@ func Test_helmExternal_Observe(t *testing.T) { client: resource.ClientApplicator{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { - *obj.(*unstructured.Unstructured) = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "crossplane-system", - "annotations": map[string]interface{}{ - corev1.LastAppliedConfigAnnotation: `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system", "labels": {"old-label":"gone"}}}`, - }, - }, - }, - } + *obj.(*unstructured.Unstructured) = + *externalResourceWithLastAppliedConfigAnnotation( + `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system", "labels": {"old-label":"gone"}}}`, + ) return nil }), }, @@ -514,18 +596,10 @@ func Test_helmExternal_Observe(t *testing.T) { client: resource.ClientApplicator{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { - *obj.(*unstructured.Unstructured) = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": testObjectName, - "annotations": map[string]interface{}{ - corev1.LastAppliedConfigAnnotation: `{"apiVersion":"v1","kind":"Namespace"}`, - }, - }, - }, - } + *obj.(*unstructured.Unstructured) = + *externalResourceWithLastAppliedConfigAnnotation( + `{"apiVersion":"v1","kind":"Namespace"}`, + ) return nil }), }, @@ -542,18 +616,10 @@ func Test_helmExternal_Observe(t *testing.T) { client: resource.ClientApplicator{ Client: &test.MockClient{ MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { - *obj.(*unstructured.Unstructured) = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "Namespace", - "metadata": map[string]interface{}{ - "name": "crossplane-system", - "annotations": map[string]interface{}{ - corev1.LastAppliedConfigAnnotation: `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system"}}`, - }, - }, - }, - } + *obj.(*unstructured.Unstructured) = + *externalResourceWithLastAppliedConfigAnnotation( + `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system"}}`, + ) return nil }), }, @@ -564,12 +630,169 @@ func Test_helmExternal_Observe(t *testing.T) { err: nil, }, }, + "UpToDateIfManagementPolicyDefined": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.ManagementPolicy = "ObserveDelete" + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = *externalResource() + return nil + }), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, + err: nil, + }, + }, + "FailedToPatchFieldFromReferenceObject": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + obj.Spec.References[0].PatchesFrom.FieldPath = pointer.StringPtr("nonexistent_field") + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + }), + }, + }, + }, + want: want{ + err: errors.Wrap( + errors.Wrap(errors.Errorf(`nonexistent_field: no such field`), + errPatchFromReferencedResource), errResolveResourceReferences), + }, + }, + "NoReferenceObjectExists": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + }, + }, + }, + want: want{ + err: errors.Wrap( + errors.Wrap(errBoom, + errGetReferencedResource), errResolveResourceReferences), + }, + }, + "NoExternalResourceExistsIfObjectWasDeleted": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == testReferenceObjectName { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + } else if key.Name == "crossplane-system" { + return kerrors.NewNotFound(schema.GroupResource{}, "") + } + return errBoom + }, + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: false}, + err: nil, + }, + }, + "NoExternalResourceDeletableIfObjectWasDeleted": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} + obj.Spec.ManagementPolicy = "ObserveCreateUpdate" + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == testReferenceObjectName { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + } else if key.Name == "crossplane-system" { + *obj.(*unstructured.Unstructured) = *externalResource() + return nil + } + return errBoom + }, + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: false}, + err: nil, + }, + }, + "ReferenceToObject": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if key.Name == testReferenceObjectName { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + } else if key.Name == "crossplane-system" { + *obj.(*unstructured.Unstructured) = + *externalResourceWithLastAppliedConfigAnnotation( + `{"apiVersion":"v1","kind":"Namespace","metadata":{"name":"crossplane-system"}}`, + ) + return nil + } + return errBoom + }, + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, + err: nil, + }, + }, + "EmptyReference": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = []v1alpha1.Reference{{}} + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, + err: nil, + }, + }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { e := &external{ - logger: logging.NewNopLogger(), - client: tc.args.client, + logger: logging.NewNopLogger(), + client: tc.args.client, + localClient: tc.args.client, } got, gotErr := e.Observe(context.Background(), tc.args.mg) if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { @@ -627,6 +850,17 @@ func Test_helmExternal_Create(t *testing.T) { err: errors.Wrap(errBoom, errCreateObject), }, }, + "SkipCreateIfManagementPolicyDefined": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.ManagementPolicy = "ObserveDelete" + }), + }, + want: want{ + out: managed.ExternalCreation{}, + err: nil, + }, + }, "SuccessDefaultsToObjectName": { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { @@ -735,6 +969,17 @@ func Test_helmExternal_Update(t *testing.T) { err: errors.Wrap(errBoom, errApplyObject), }, }, + "SkipUpdateIfManagementPolicyDefined": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.ManagementPolicy = "ObserveDelete" + }), + }, + want: want{ + out: managed.ExternalUpdate{}, + err: nil, + }, + }, "SuccessDefaultsToObjectName": { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { @@ -830,6 +1075,16 @@ func Test_helmExternal_Delete(t *testing.T) { err: errors.Wrap(errBoom, errDeleteObject), }, }, + "SkipDeleteIfManagementPolicyDefined": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.ManagementPolicy = "ObserveCreateUpdate" + }), + }, + want: want{ + err: nil, + }, + }, "SuccessDefaultsToObjectName": { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { @@ -879,3 +1134,263 @@ func Test_helmExternal_Delete(t *testing.T) { }) } } + +func Test_objFinalizer_AddFinalizer(t *testing.T) { + type args struct { + client resource.ClientApplicator + mg resource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "NotKubernetesObject": { + args: args{ + mg: notKubernetesObject{}, + }, + want: want{ + err: errors.New(errNotKubernetesObject), + }, + }, + "FailedToAddObjectFinalizer": { + args: args{ + mg: kubernetesObject(), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(errBoom), + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errAddFinalizer), + }, + }, + "ObjectFinalizerExists": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) + }), + }, + want: want{ + err: nil, + }, + }, + "NoReferenceObjectExists": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(errBoom), + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + err: errors.Wrap( + errors.Wrap(errBoom, + errGetReferencedResource), errAddFinalizer), + }, + }, + "EmptyReference": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = []v1alpha1.Reference{{}} + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + err: nil, + }, + }, + "FailedToAddReferenceFinalizer": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + }), + MockUpdate: test.NewMockUpdateFn(nil, func(obj client.Object) error { + name := obj.GetName() + if name == testReferenceObjectName { + return errBoom + } + return nil + }), + }, + }, + }, + want: want{ + err: errors.Wrap( + errors.Wrap(errBoom, + errAddReferenceFinalizer), errAddFinalizer), + }, + }, + "Success": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil), + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + f := &objFinalizer{ + client: tc.args.client, + } + gotErr := f.AddFinalizer(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("f.AddFinalizer(...): -want error, +got error: %s", diff) + } + }) + } +} + +func Test_objFinalizer_RemoveFinalizer(t *testing.T) { + type args struct { + client resource.ClientApplicator + mg resource.Managed + } + type want struct { + err error + } + cases := map[string]struct { + args + want + }{ + "NotKubernetesObject": { + args: args{ + mg: notKubernetesObject{}, + }, + want: want{ + err: errors.New(errNotKubernetesObject), + }, + }, + "FailedToRemoveObjectFinalizer": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockUpdate: test.NewMockUpdateFn(errBoom), + }, + }, + }, + want: want{ + err: errors.Wrap(errBoom, errRemoveFinalizer), + }, + }, + "NoObjectFinalizerExists": { + args: args{ + mg: kubernetesObject(), + }, + want: want{ + err: nil, + }, + }, + "NoReferenceFinalizerExists": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) + obj.Spec.References = objectReferences() + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = *referenceObject() + return nil + }), + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + err: nil, + }, + }, + "FailedToRemoveReferenceFinalizer": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) + obj.Spec.References = objectReferences() + obj.ObjectMeta.UID = "some-uid" + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + *obj.(*unstructured.Unstructured) = *referenceObjectWithFinalizer(refFinalizerNamePrefix + "some-uid") + return nil + }, + MockUpdate: test.NewMockUpdateFn(nil, func(obj client.Object) error { + name := obj.GetName() + if name == testReferenceObjectName { + return errBoom + } + return nil + }), + }, + }, + }, + want: want{ + err: errors.Wrap( + errors.Wrap(errBoom, + errRemoveReferenceFinalizer), errRemoveFinalizer), + }, + }, + "Success": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.ObjectMeta.Finalizers = append(obj.ObjectMeta.Finalizers, objFinalizerName) + obj.Spec.References = objectReferences() + obj.ObjectMeta.UID = "some-uid" + }), + client: resource.ClientApplicator{ + Client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*unstructured.Unstructured) = *referenceObjectWithFinalizer(refFinalizerNamePrefix + "some-uid") + return nil + }), + MockUpdate: test.NewMockUpdateFn(nil), + }, + }, + }, + want: want{ + err: nil, + }, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + f := &objFinalizer{ + client: tc.args.client, + } + gotErr := f.RemoveFinalizer(context.Background(), tc.args.mg) + if diff := cmp.Diff(tc.want.err, gotErr, test.EquateErrors()); diff != "" { + t.Fatalf("f.RemoveFinalizer(...): -want error, +got error: %s", diff) + } + }) + } +} diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index d3dacdcb..37386309 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -61,6 +61,15 @@ spec: required: - manifest type: object + managementPolicy: + default: Default + description: A ManagementPolicy determines what should happen to the underlying external resource when a managed resource is created, updated, deleted, or observed. + enum: + - Default + - ObserveCreateUpdate + - ObserveDelete + - Observe + type: string providerConfigRef: description: ProviderConfigReference specifies how the provider that will be used to create, observe, update, and delete this managed resource should be configured. properties: @@ -79,6 +88,59 @@ spec: required: - name type: object + references: + items: + description: Reference refers to an Object or arbitrary Kubernetes resource and optionally patch values from that resource to the current Object. + properties: + dependsOn: + description: DependsOn is used to declare dependency on other Object or arbitrary Kubernetes resource. + properties: + apiVersion: + default: kubernetes.crossplane.io/v1alpha1 + description: APIVersion of the referenced object. + type: string + kind: + default: Object + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object. + type: string + required: + - name + type: object + patchesFrom: + description: PatchesFrom is used to declare dependency on other Object or arbitrary Kubernetes resource, and also patch fields from this object. + properties: + apiVersion: + default: kubernetes.crossplane.io/v1alpha1 + description: APIVersion of the referenced object. + type: string + fieldPath: + description: FieldPath is the path of the field on the resource whose value is to be used as input. + type: string + kind: + default: Object + description: Kind of the referenced object. + type: string + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object. + type: string + required: + - fieldPath + - name + type: object + toFieldPath: + description: ToFieldPath is the path of the field on the resource whose value will be changed with the result of transforms. Leave empty if you'd like to propagate to the same path as patchesFrom.fieldPath. + type: string + type: object + type: array writeConnectionSecretToRef: description: WriteConnectionSecretToReference specifies the namespace and name of a Secret to which any connection details for this managed resource should be written. Connection details frequently include the endpoint, username, and password required to connect to the managed resource. properties: diff --git a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml index fd592279..60d9345b 100644 --- a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml +++ b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml @@ -8,6 +8,10 @@ metadata: spec: group: kubernetes.crossplane.io names: + categories: + - crossplane + - provider + - kubernetes kind: ProviderConfig listKind: ProviderConfigList plural: providerconfigs