From ef88fd61848c472bf53c9ee3497dc6739a6b0266 Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Mon, 3 Jan 2022 23:41:19 +0800 Subject: [PATCH 01/11] init commit for feature mgmt policy and reference Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 96 ++++++++ apis/object/v1alpha1/zz_generated.deepcopy.go | 48 ++++ internal/controller/object/object.go | 211 ++++++++++++++++-- .../kubernetes.crossplane.io_objects.yaml | 42 ++++ 4 files changed, 377 insertions(+), 20 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 0f9fed32..f057071b 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -18,10 +18,61 @@ 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" ) +// 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" +) + +// FromObject 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 FromObject struct { + // APIVersion of the referenced object. + APIVersion string `json:"apiVersion"` + // Kind of the referenced object. + Kind string `json:"kind"` + // Name of the referenced object. + Name string `json:"name"` + // Namespace of the referenced object. + // +optional + Namespace string `json:"namespace,omitempty"` + // FieldPath is the path of the field on the resource whose value is to be + // used as input. + // +optional + FieldPath *string `json:"fieldPath,omitempty"` +} + +// Reference refers to an Object or arbitrary Kubernetes resource and optionally +// patch values from that resource to the current Object. +type Reference struct { + // FromObject is the reference of other Object or arbitrary Kubernetes + // resource + FromObject `json:"fromObject"` + // 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 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,6 +92,8 @@ type ObjectObservation struct { // A ObjectSpec defines the desired state of a Object. type ObjectSpec struct { xpv1.ResourceSpec `json:",inline"` + ManagementPolicy `json:"managementPolicy,omitempty"` + References []Reference `json:"references,omitempty"` ForProvider ObjectParameters `json:"forProvider"` } @@ -74,3 +127,46 @@ 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 { + if r.FromObject.FieldPath == nil { + return nil + } + + // Default to patch the same field on the "to" resource. + if r.ToFieldPath == nil { + r.ToFieldPath = r.FromObject.FieldPath + } + + fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from) + if err != nil { + return err + } + + out, err := fieldpath.Pave(fromMap).GetValue(*r.FromObject.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 { + if u, ok := to.(interface{ UnstructuredContent() map[string]interface{} }); ok { + return fieldpath.Pave(u.UnstructuredContent()).SetValue(path, value) + } + + toMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(to) + if err != nil { + return err + } + if err := fieldpath.Pave(toMap).SetValue(path, value); err != nil { + return err + } + return runtime.DefaultUnstructuredConverter.FromUnstructured(toMap, to) +} diff --git a/apis/object/v1alpha1/zz_generated.deepcopy.go b/apis/object/v1alpha1/zz_generated.deepcopy.go index 84fe4096..643c4121 100644 --- a/apis/object/v1alpha1/zz_generated.deepcopy.go +++ b/apis/object/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,26 @@ 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 *FromObject) DeepCopyInto(out *FromObject) { + *out = *in + 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 FromObject. +func (in *FromObject) DeepCopy() *FromObject { + if in == nil { + return nil + } + out := new(FromObject) + 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 +139,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 +175,24 @@ 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 *Reference) DeepCopyInto(out *Reference) { + *out = *in + in.FromObject.DeepCopyInto(&out.FromObject) + 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/internal/controller/object/object.go b/internal/controller/object/object.go index 662bf575..34ee53f5 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -65,6 +65,13 @@ 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" + + errFailedToAddFinalizer = "Failed to add finalizer to referenced resource" + errFailedToRemoveFinalizer = "Failed to remove finalizer to referenced resource" ) // Setup adds a controller that reconciles Object managed resources. @@ -175,12 +182,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 +201,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 +217,10 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex Name: observed.GetName(), }, observed) - if kerrors.IsNotFound(err) { + if c.isNotFound(ctx, cr, err) { return managed.ExternalObservation{ResourceExists: false}, nil } + if err != nil { return managed.ExternalObservation{}, errors.Wrap(err, errGetObject) } @@ -218,25 +233,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 +243,12 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } c.logger.Debug("Creating", "resource", cr) + + if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + 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 @@ -271,6 +274,11 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Updating", "resource", cr) + if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + 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 @@ -295,6 +303,12 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { } c.logger.Debug("Deleting", "resource", cr) + + if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveCreateUpdate || policy == v1alpha1.Observe { + c.logger.Debug("External resource should not be deleted by provider, skip updating.") + return nil + } + obj, err := getDesired(cr) if err != nil { return err @@ -340,3 +354,160 @@ func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) erro } return nil } + +// 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 { + var kubeClient client.Client + + if ref.FromObject.APIVersion == obj.APIVersion && ref.FromObject.Kind == obj.Kind { + // Object resource handled by provider on local cluster + kubeClient = c.localClient + } else { + // Other resource usually handled on remote cluster + // But could also be handled on local cluster + kubeClient = c.client + } + + res := &unstructured.Unstructured{} + res.SetAPIVersion(ref.FromObject.APIVersion) + res.SetKind(ref.FromObject.Kind) + // Try to get referenced resource + err := kubeClient.Get(ctx, client.ObjectKey{ + Namespace: ref.FromObject.Namespace, + Name: ref.FromObject.Name, + }, res) + + if err != nil { + return errors.Wrap(err, errGetReferencedResource) + } + + // Patch fields if any + if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { + return errors.Wrap(err, errPatchFromReferencedResource) + } + + // Add finalizer if not exists + finalizer := "object/" + string(obj.UID) + if !meta.FinalizerExists(res, finalizer) { + meta.AddFinalizer(res, finalizer) + if err := kubeClient.Update(ctx, res); err != nil { + return errors.Wrap(err, errFailedToAddFinalizer) + } + } + } + + return nil +} + +// removeReferenceFinalizers removes finalizers previously added to references. +func (c *external) removeReferenceFinalizers(ctx context.Context, obj *v1alpha1.Object) { + c.logger.Debug("Removing finalizers from referencies.") + + // Loop through references to resolve each referenced resource + for _, ref := range obj.Spec.References { + var kubeClient client.Client + + if ref.FromObject.APIVersion == obj.APIVersion && ref.FromObject.Kind == obj.Kind { + // Object resource handled by provider on local cluster + kubeClient = c.localClient + } else { + // Other resource usually handled on remote cluster + // But could also be handled on local cluster + kubeClient = c.client + } + + res := &unstructured.Unstructured{} + res.SetAPIVersion(ref.FromObject.APIVersion) + res.SetKind(ref.FromObject.Kind) + // Try to get referenced resource + err := kubeClient.Get(ctx, client.ObjectKey{ + Namespace: ref.FromObject.Namespace, + Name: ref.FromObject.Name, + }, res) + + if err != nil { + c.logger.Debug(errGetReferencedResource, "error", err) + continue + } + + // Remove finalizer if exists + finalizer := "object/" + string(obj.UID) + if meta.FinalizerExists(res, finalizer) { + meta.RemoveFinalizer(res, finalizer) + if err := kubeClient.Update(ctx, res); err != nil { + c.logger.Debug(errFailedToRemoveFinalizer, "error", err) + continue + } + } + } +} + +func (c *external) isNotFound(ctx context.Context, 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 policy := obj.Spec.ManagementPolicy; policy == v1alpha1.ObserveCreateUpdate || policy == v1alpha1.Observe { + c.logger.Debug("Managed resource was deleted but external resource is undeletable.") + isNotFound = true + } + } + + if meta.WasDeleted(obj) && isNotFound { + // If the external resource is not found, we should remove the finalizer + // that was previously added to the references of this Object, since the + // Object, as the dependant resource, was deleted. + c.removeReferenceFinalizers(ctx, obj) + } + + return isNotFound +} + +func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { + policy := obj.Spec.ManagementPolicy + isUpToDate := false + + if policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + // 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!") + + // We usually set condition available after create or update is invoked. + // However, it will never happen when policy ObserveDelete or Observe is + // specified since it does not allow us to create or update the external + // resource. In such a case, let's set condition available explicitly if + // the resource has been up to date. + if policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + obj.Status.SetConditions(xpv1.Available()) + } + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, nil + } + + return managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + }, nil +} diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index d3dacdcb..98fd982f 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -61,6 +61,14 @@ spec: required: - manifest type: object + managementPolicy: + 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 +87,40 @@ 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: + fromObject: + description: FromObject is the reference of other Object or arbitrary Kubernetes resource + properties: + apiVersion: + 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: + 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: + - apiVersion + - kind + - 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 fieldPath. + type: string + required: + - fromObject + 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: From e8ea770515f922d09798243ff01c99170f383f6b Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Wed, 5 Jan 2022 14:47:21 +0800 Subject: [PATCH 02/11] add test cases Signed-off-by: Ying Mo --- go.mod | 1 + internal/controller/object/object_test.go | 395 +++++++++++++++++++--- 2 files changed, 347 insertions(+), 49 deletions(-) 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_test.go b/internal/controller/object/object_test.go index 6f98de35..6e7727f9 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,9 +65,14 @@ 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, @@ -95,6 +103,104 @@ 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 { + ref := []v1alpha1.Reference{ + { + FromObject: v1alpha1.FromObject{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: v1alpha1.ObjectKind, + Name: testReferenceObjectName, + Namespace: testNamespace, + }, + }, + } + return ref +} + +func arbitraryResourceReferences() []v1alpha1.Reference { + ref := []v1alpha1.Reference{ + { + FromObject: v1alpha1.FromObject{ + APIVersion: "v1", + Kind: "ConfigMap", + Name: testReferenceObjectName, + Namespace: testNamespace, + }, + }, + } + return ref +} + +func referenceObject() *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", + }, + }, + }, + }, + }, + }, + } + return obj +} + +func referenceArbitraryResource() *unstructured.Unstructured { + res := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": testReferenceObjectName, + "finalizers": []interface{}{"object/some-uid"}, + }, + }, + } + return res +} + type providerConfigModifier func(pc *kubernetesv1alpha1.ProviderConfig) func providerConfig(pm ...providerConfigModifier) *kubernetesv1alpha1.ProviderConfig { @@ -457,15 +563,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 +580,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 +604,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 +624,29 @@ 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 + }), + }, + }, + }, + want: want{ + out: managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: true}, + 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 }), }, @@ -564,12 +657,184 @@ func Test_helmExternal_Observe(t *testing.T) { err: nil, }, }, + "FailedToPatchFieldFromReferenceObject": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = objectReferences() + obj.Spec.References[0].FromObject.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), + }, + }, + "FailedToAddFinalizerToReferenceObject": { + 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(errBoom), + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Wrap( + errBoom, errFailedToAddFinalizer), 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.ObjectMeta.UID = "some-uid" + obj.Spec.ManagementPolicy = "ObserveCreateUpdate" + obj.Spec.References = arbitraryResourceReferences() + }), + 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) = *referenceArbitraryResource() + 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, + }, + }, + "ReferenceToArbitraryResource": { + args: args{ + mg: kubernetesObject(func(obj *v1alpha1.Object) { + obj.Spec.References = arbitraryResourceReferences() + }), + 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) = *referenceArbitraryResource() + 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, + }, + }, } 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 +892,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 +1011,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 +1117,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) { From 7287e6b338636a39919499d6e102bf19d43ac24b Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Sat, 8 Jan 2022 09:52:53 +0800 Subject: [PATCH 03/11] refine per review comments Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 40 ++- internal/controller/object/object.go | 202 +++++++------ internal/controller/object/object_test.go | 336 +++++++++++++++++----- 3 files changed, 396 insertions(+), 182 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index f057071b..5651d1f6 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -23,6 +23,9 @@ import ( "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 @@ -39,6 +42,10 @@ const ( ObserveDelete ManagementPolicy = "ObserveDelete" // Observe means the provider can only observe the resource. Observe ManagementPolicy = "Observe" + + CreateObject ObjectAction = "CreateObject" + UpdateObject ObjectAction = "UpdateObject" + DeleteObject ObjectAction = "DeleteObject" ) // FromObject refers to an object by Name, Kind, APIVersion, etc. It is used @@ -68,7 +75,7 @@ type Reference struct { FromObject `json:"fromObject"` // 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 fieldPath. + // propagate to the same path as fromObject.fieldPath. // +optional ToFieldPath *string `json:"toFieldPath,omitempty"` } @@ -131,21 +138,17 @@ type ObjectList struct { // ApplyFromFieldPathPatch patches the "to" resource, using a source field // on the "from" resource. func (r *Reference) ApplyFromFieldPathPatch(from, to runtime.Object) error { - if r.FromObject.FieldPath == nil { - return nil - } - // Default to patch the same field on the "to" resource. if r.ToFieldPath == nil { r.ToFieldPath = r.FromObject.FieldPath } - fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from) + paved, err := fieldpath.PaveObject(from) if err != nil { return err } - out, err := fieldpath.Pave(fromMap).GetValue(*r.FromObject.FieldPath) + out, err := paved.GetValue(*r.FromObject.FieldPath) if err != nil { return err } @@ -157,16 +160,27 @@ func (r *Reference) ApplyFromFieldPathPatch(from, to runtime.Object) error { // 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 { - if u, ok := to.(interface{ UnstructuredContent() map[string]interface{} }); ok { - return fieldpath.Pave(u.UnstructuredContent()).SetValue(path, value) + paved, err := fieldpath.PaveObject(to) + if err != nil { + return err } - toMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(to) + err = paved.SetValue(path, value) if err != nil { return err } - if err := fieldpath.Pave(toMap).SetValue(path, value); err != nil { - return err + + return runtime.DefaultUnstructuredConverter.FromUnstructured(paved.UnstructuredContent(), to) +} + +func (p *ManagementPolicy) IsActionAllowed(action ObjectAction) bool { + if *p == "" { + *p = Default + } + + if action == CreateObject || action == UpdateObject { + return *p == Default || *p == ObserveCreateUpdate + } else { + return *p == Default || *p == ObserveDelete } - return runtime.DefaultUnstructuredConverter.FromUnstructured(toMap, to) } diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 34ee53f5..a14582ea 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -70,8 +70,12 @@ const ( errPatchFromReferencedResource = "cannot patch from referenced resource" errResolveResourceReferences = "cannot resolve resource references" - errFailedToAddFinalizer = "Failed to add finalizer to referenced resource" - errFailedToRemoveFinalizer = "Failed to remove finalizer to referenced resource" + 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. @@ -96,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)))) @@ -244,7 +249,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Creating", "resource", cr) - if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.CreateObject) { c.logger.Debug("External resource should not be created by provider, skip creating.") return managed.ExternalCreation{}, nil } @@ -262,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) } @@ -274,7 +278,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Updating", "resource", cr) - if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.UpdateObject) { c.logger.Debug("External resource should not be updated by provider, skip updating.") return managed.ExternalUpdate{}, nil } @@ -292,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) } @@ -304,8 +307,8 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { c.logger.Debug("Deleting", "resource", cr) - if policy := cr.Spec.ManagementPolicy; policy == v1alpha1.ObserveCreateUpdate || policy == v1alpha1.Observe { - c.logger.Debug("External resource should not be deleted by provider, skip updating.") + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.DeleteObject) { + c.logger.Debug("External resource should not be deleted by provider, skip deleting.") return nil } @@ -363,22 +366,11 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) // Loop through references to resolve each referenced resource for _, ref := range obj.Spec.References { - var kubeClient client.Client - - if ref.FromObject.APIVersion == obj.APIVersion && ref.FromObject.Kind == obj.Kind { - // Object resource handled by provider on local cluster - kubeClient = c.localClient - } else { - // Other resource usually handled on remote cluster - // But could also be handled on local cluster - kubeClient = c.client - } - res := &unstructured.Unstructured{} res.SetAPIVersion(ref.FromObject.APIVersion) res.SetKind(ref.FromObject.Kind) // Try to get referenced resource - err := kubeClient.Get(ctx, client.ObjectKey{ + err := c.localClient.Get(ctx, client.ObjectKey{ Namespace: ref.FromObject.Namespace, Name: ref.FromObject.Name, }, res) @@ -388,16 +380,9 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) } // Patch fields if any - if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { - return errors.Wrap(err, errPatchFromReferencedResource) - } - - // Add finalizer if not exists - finalizer := "object/" + string(obj.UID) - if !meta.FinalizerExists(res, finalizer) { - meta.AddFinalizer(res, finalizer) - if err := kubeClient.Update(ctx, res); err != nil { - return errors.Wrap(err, errFailedToAddFinalizer) + if ref.FromObject.FieldPath != nil { + if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { + return errors.Wrap(err, errPatchFromReferencedResource) } } } @@ -405,49 +390,6 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) return nil } -// removeReferenceFinalizers removes finalizers previously added to references. -func (c *external) removeReferenceFinalizers(ctx context.Context, obj *v1alpha1.Object) { - c.logger.Debug("Removing finalizers from referencies.") - - // Loop through references to resolve each referenced resource - for _, ref := range obj.Spec.References { - var kubeClient client.Client - - if ref.FromObject.APIVersion == obj.APIVersion && ref.FromObject.Kind == obj.Kind { - // Object resource handled by provider on local cluster - kubeClient = c.localClient - } else { - // Other resource usually handled on remote cluster - // But could also be handled on local cluster - kubeClient = c.client - } - - res := &unstructured.Unstructured{} - res.SetAPIVersion(ref.FromObject.APIVersion) - res.SetKind(ref.FromObject.Kind) - // Try to get referenced resource - err := kubeClient.Get(ctx, client.ObjectKey{ - Namespace: ref.FromObject.Namespace, - Name: ref.FromObject.Name, - }, res) - - if err != nil { - c.logger.Debug(errGetReferencedResource, "error", err) - continue - } - - // Remove finalizer if exists - finalizer := "object/" + string(obj.UID) - if meta.FinalizerExists(res, finalizer) { - meta.RemoveFinalizer(res, finalizer) - if err := kubeClient.Update(ctx, res); err != nil { - c.logger.Debug(errFailedToRemoveFinalizer, "error", err) - continue - } - } - } -} - func (c *external) isNotFound(ctx context.Context, obj *v1alpha1.Object, err error) bool { isNotFound := false @@ -459,27 +401,19 @@ func (c *external) isNotFound(ctx context.Context, obj *v1alpha1.Object, err err // 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 policy := obj.Spec.ManagementPolicy; policy == v1alpha1.ObserveCreateUpdate || policy == v1alpha1.Observe { + if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.DeleteObject) { c.logger.Debug("Managed resource was deleted but external resource is undeletable.") isNotFound = true } } - if meta.WasDeleted(obj) && isNotFound { - // If the external resource is not found, we should remove the finalizer - // that was previously added to the references of this Object, since the - // Object, as the dependant resource, was deleted. - c.removeReferenceFinalizers(ctx, obj) - } - return isNotFound } func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { - policy := obj.Spec.ManagementPolicy isUpToDate := false - if policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { + if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.UpdateObject) { // Treated as up-to-date to skip last applied annotation update since we // do not create or update the external resource. isUpToDate = true @@ -491,14 +425,7 @@ func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstru if isUpToDate { c.logger.Debug("Up to date!") - // We usually set condition available after create or update is invoked. - // However, it will never happen when policy ObserveDelete or Observe is - // specified since it does not allow us to create or update the external - // resource. In such a case, let's set condition available explicitly if - // the resource has been up to date. - if policy == v1alpha1.ObserveDelete || policy == v1alpha1.Observe { - obj.Status.SetConditions(xpv1.Available()) - } + obj.Status.SetConditions(xpv1.Available()) return managed.ExternalObservation{ ResourceExists: true, @@ -511,3 +438,96 @@ func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstru 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 { + res := &unstructured.Unstructured{} + res.SetAPIVersion(ref.FromObject.APIVersion) + res.SetKind(ref.FromObject.Kind) + // Try to get referenced resource + err := f.client.Get(ctx, client.ObjectKey{ + Namespace: ref.FromObject.Namespace, + Name: ref.FromObject.Name, + }, 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 6e7727f9..3482684b 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -145,21 +145,7 @@ func objectReferences() []v1alpha1.Reference { return ref } -func arbitraryResourceReferences() []v1alpha1.Reference { - ref := []v1alpha1.Reference{ - { - FromObject: v1alpha1.FromObject{ - APIVersion: "v1", - Kind: "ConfigMap", - Name: testReferenceObjectName, - Namespace: testNamespace, - }, - }, - } - return ref -} - -func referenceObject() *unstructured.Unstructured { +func referenceObject(rm ...externalResourceModifier) *unstructured.Unstructured { obj := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": v1alpha1.SchemeGroupVersion.String(), @@ -184,20 +170,19 @@ func referenceObject() *unstructured.Unstructured { }, }, } + + for _, m := range rm { + m(obj) + } + return obj } -func referenceArbitraryResource() *unstructured.Unstructured { - res := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": testReferenceObjectName, - "finalizers": []interface{}{"object/some-uid"}, - }, - }, - } +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 } @@ -678,26 +663,6 @@ func Test_helmExternal_Observe(t *testing.T) { errPatchFromReferencedResource), errResolveResourceReferences), }, }, - "FailedToAddFinalizerToReferenceObject": { - 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(errBoom), - }, - }, - }, - want: want{ - err: errors.Wrap(errors.Wrap( - errBoom, errFailedToAddFinalizer), errResolveResourceReferences), - }, - }, "NoReferenceObjectExists": { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { @@ -745,15 +710,14 @@ func Test_helmExternal_Observe(t *testing.T) { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { obj.ObjectMeta.DeletionTimestamp = &metav1.Time{Time: time.Now()} - obj.ObjectMeta.UID = "some-uid" obj.Spec.ManagementPolicy = "ObserveCreateUpdate" - obj.Spec.References = arbitraryResourceReferences() + 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) = *referenceArbitraryResource() + *obj.(*unstructured.Unstructured) = *referenceObject() return nil } else if key.Name == "crossplane-system" { *obj.(*unstructured.Unstructured) = *externalResource() @@ -799,35 +763,6 @@ func Test_helmExternal_Observe(t *testing.T) { err: nil, }, }, - "ReferenceToArbitraryResource": { - args: args{ - mg: kubernetesObject(func(obj *v1alpha1.Object) { - obj.Spec.References = arbitraryResourceReferences() - }), - 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) = *referenceArbitraryResource() - 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, - }, - }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -1176,3 +1111,248 @@ 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), + }, + }, + "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) + } + }) + } +} From ed3790bf93d0dfad727db05d97206a81b5b00a8e Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Sat, 8 Jan 2022 10:12:46 +0800 Subject: [PATCH 04/11] fix lint errors Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 8 ++++++-- internal/controller/object/object.go | 4 ++-- package/crds/kubernetes.crossplane.io_objects.yaml | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 5651d1f6..93193c6f 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -43,8 +43,11 @@ const ( // Observe means the provider can only observe the resource. Observe ManagementPolicy = "Observe" + // CreateObject means to create an Object CreateObject ObjectAction = "CreateObject" + // UpdateObject means to update an Object UpdateObject ObjectAction = "UpdateObject" + // DeleteObject means to delete an Object DeleteObject ObjectAction = "DeleteObject" ) @@ -173,6 +176,7 @@ func patchFieldValueToObject(path string, value interface{}, to runtime.Object) 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 *p == "" { *p = Default @@ -180,7 +184,7 @@ func (p *ManagementPolicy) IsActionAllowed(action ObjectAction) bool { if action == CreateObject || action == UpdateObject { return *p == Default || *p == ObserveCreateUpdate - } else { - return *p == Default || *p == ObserveDelete } + + return *p == Default || *p == ObserveDelete } diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index a14582ea..a03d239f 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -222,7 +222,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex Name: observed.GetName(), }, observed) - if c.isNotFound(ctx, cr, err) { + if c.isNotFound(cr, err) { return managed.ExternalObservation{ResourceExists: false}, nil } @@ -390,7 +390,7 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) return nil } -func (c *external) isNotFound(ctx context.Context, obj *v1alpha1.Object, err error) bool { +func (c *external) isNotFound(obj *v1alpha1.Object, err error) bool { isNotFound := false if kerrors.IsNotFound(err) { diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index 98fd982f..6e958bcb 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -115,7 +115,7 @@ spec: - 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 fieldPath. + 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 fromObject.fieldPath. type: string required: - fromObject From c753317f40c817a11d1447f52408a4963462642a Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Sat, 8 Jan 2022 12:34:59 +0800 Subject: [PATCH 05/11] small refinements Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 26 +++++++++---------- internal/controller/object/object.go | 10 +++---- internal/controller/object/object_test.go | 1 + .../kubernetes.crossplane.io_objects.yaml | 1 + 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 93193c6f..48a8558a 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -43,12 +43,12 @@ const ( // Observe means the provider can only observe the resource. Observe ManagementPolicy = "Observe" - // CreateObject means to create an Object - CreateObject ObjectAction = "CreateObject" - // UpdateObject means to update an Object - UpdateObject ObjectAction = "UpdateObject" - // DeleteObject means to delete an Object - DeleteObject ObjectAction = "DeleteObject" + // 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" ) // FromObject refers to an object by Name, Kind, APIVersion, etc. It is used @@ -102,9 +102,10 @@ type ObjectObservation struct { // A ObjectSpec defines the desired state of a Object. type ObjectSpec struct { xpv1.ResourceSpec `json:",inline"` - ManagementPolicy `json:"managementPolicy,omitempty"` - References []Reference `json:"references,omitempty"` - 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. @@ -178,13 +179,10 @@ func patchFieldValueToObject(path string, value interface{}, to runtime.Object) // IsActionAllowed determines if action is allowed to be performed on Object func (p *ManagementPolicy) IsActionAllowed(action ObjectAction) bool { - if *p == "" { - *p = Default - } - - if action == CreateObject || action == UpdateObject { + if action == ObjectActionCreate || action == ObjectActionUpdate { return *p == Default || *p == ObserveCreateUpdate } + // ObjectActionDelete return *p == Default || *p == ObserveDelete } diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index a03d239f..89cc5baa 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -249,7 +249,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Creating", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.CreateObject) { + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionCreate) { c.logger.Debug("External resource should not be created by provider, skip creating.") return managed.ExternalCreation{}, nil } @@ -278,7 +278,7 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext c.logger.Debug("Updating", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.UpdateObject) { + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionUpdate) { c.logger.Debug("External resource should not be updated by provider, skip updating.") return managed.ExternalUpdate{}, nil } @@ -307,7 +307,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { c.logger.Debug("Deleting", "resource", cr) - if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.DeleteObject) { + if !cr.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { c.logger.Debug("External resource should not be deleted by provider, skip deleting.") return nil } @@ -401,7 +401,7 @@ func (c *external) isNotFound(obj *v1alpha1.Object, err error) bool { // 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.DeleteObject) { + if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.ObjectActionDelete) { c.logger.Debug("Managed resource was deleted but external resource is undeletable.") isNotFound = true } @@ -413,7 +413,7 @@ func (c *external) isNotFound(obj *v1alpha1.Object, err error) bool { func (c *external) handleLastApplied(obj *v1alpha1.Object, last, desired *unstructured.Unstructured) (managed.ExternalObservation, error) { isUpToDate := false - if !obj.Spec.ManagementPolicy.IsActionAllowed(v1alpha1.UpdateObject) { + 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 diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 3482684b..0b31f49d 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -78,6 +78,7 @@ func kubernetesObject(om ...kubernetesObjectModifier) *v1alpha1.Object { Namespace: testNamespace, }, Spec: v1alpha1.ObjectSpec{ + ManagementPolicy: v1alpha1.Default, ResourceSpec: xpv1.ResourceSpec{ ProviderConfigReference: &xpv1.Reference{ Name: providerName, diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index 6e958bcb..6b84da7d 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -62,6 +62,7 @@ spec: - 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 From 57ab7bd2793e1820acd52f55f48c4d6f977b6f16 Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Tue, 15 Feb 2022 16:10:30 +0800 Subject: [PATCH 06/11] Use dependsOn & patchesFrom and default to Object Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 38 ++++++++++----- apis/object/v1alpha1/zz_generated.deepcopy.go | 37 ++++++++++---- internal/controller/object/object.go | 48 +++++++++++++++---- internal/controller/object/object_test.go | 15 +++--- .../kubernetes.crossplane.io_objects.yaml | 33 ++++++++++--- 5 files changed, 126 insertions(+), 45 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 48a8558a..fc1f6c29 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -51,34 +51,46 @@ const ( ObjectActionDelete ObjectAction = "Delete" ) -// FromObject refers to an object by Name, Kind, APIVersion, etc. It is used -// to reference other Object or arbitrary Kubernetes resource which is either +// 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 FromObject struct { +type DependsOn struct { // APIVersion of the referenced object. - APIVersion string `json:"apiVersion"` + // +kubebuilder:default=kubernetes.crossplane.io/v1alpha1 + // +optional + APIVersion string `json:"apiVersion,omitempty"` // Kind of the referenced object. - Kind string `json:"kind"` + // +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. - // +optional - FieldPath *string `json:"fieldPath,omitempty"` + 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 { - // FromObject is the reference of other Object or arbitrary Kubernetes - // resource - FromObject `json:"fromObject"` + // DependsOn is used to declare dependency on other Object or arbitrary + // Kubernetes resource. + DependsOn `json:"dependsOn,omitempty"` + // PatchesFrom is used to declare dependency on other Object or arbitrary + // Kubernetes resource, and also patch fields from this object. + 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 fromObject.fieldPath. + // propagate to the same path as patchesFrom.fieldPath. // +optional ToFieldPath *string `json:"toFieldPath,omitempty"` } @@ -144,7 +156,7 @@ type ObjectList struct { 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.FromObject.FieldPath + r.ToFieldPath = r.PatchesFrom.FieldPath } paved, err := fieldpath.PaveObject(from) @@ -152,7 +164,7 @@ func (r *Reference) ApplyFromFieldPathPatch(from, to runtime.Object) error { return err } - out, err := paved.GetValue(*r.FromObject.FieldPath) + out, err := paved.GetValue(*r.PatchesFrom.FieldPath) if err != nil { return err } diff --git a/apis/object/v1alpha1/zz_generated.deepcopy.go b/apis/object/v1alpha1/zz_generated.deepcopy.go index 643c4121..97d37204 100644 --- a/apis/object/v1alpha1/zz_generated.deepcopy.go +++ b/apis/object/v1alpha1/zz_generated.deepcopy.go @@ -25,21 +25,16 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FromObject) DeepCopyInto(out *FromObject) { +func (in *DependsOn) DeepCopyInto(out *DependsOn) { *out = *in - 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 FromObject. -func (in *FromObject) DeepCopy() *FromObject { +// 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(FromObject) + out := new(DependsOn) in.DeepCopyInto(out) return out } @@ -176,10 +171,32 @@ func (in *ObjectStatus) DeepCopy() *ObjectStatus { 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 - in.FromObject.DeepCopyInto(&out.FromObject) + out.DependsOn = in.DependsOn + in.PatchesFrom.DeepCopyInto(&out.PatchesFrom) if in.ToFieldPath != nil { in, out := &in.ToFieldPath, &out.ToFieldPath *out = new(string) diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 89cc5baa..0af291f3 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -358,6 +358,34 @@ func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) erro return nil } +func getReferenceInfo(ref *v1alpha1.Reference) (string, string, string, string) { + // Get referenced resource APIVersion + refAPIVersion := ref.DependsOn.APIVersion + if ref.PatchesFrom.APIVersion != "" { + refAPIVersion = ref.PatchesFrom.APIVersion + } + + // Get referenced resource kind + refKind := ref.DependsOn.Kind + if ref.PatchesFrom.Kind != "" { + refKind = ref.PatchesFrom.Kind + } + + // Get referenced resource namespace + refNamespace := ref.DependsOn.Namespace + if ref.PatchesFrom.Namespace != "" { + refNamespace = ref.PatchesFrom.Namespace + } + + // Get referenced resource name + refName := ref.DependsOn.Name + if ref.PatchesFrom.Name != "" { + refName = ref.PatchesFrom.Name + } + + return refAPIVersion, refKind, refNamespace, refName +} + // 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. @@ -366,13 +394,14 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) // Loop through references to resolve each referenced resource for _, ref := range obj.Spec.References { + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(&ref) res := &unstructured.Unstructured{} - res.SetAPIVersion(ref.FromObject.APIVersion) - res.SetKind(ref.FromObject.Kind) + res.SetAPIVersion(refAPIVersion) + res.SetKind(refKind) // Try to get referenced resource err := c.localClient.Get(ctx, client.ObjectKey{ - Namespace: ref.FromObject.Namespace, - Name: ref.FromObject.Name, + Namespace: refNamespace, + Name: refName, }, res) if err != nil { @@ -380,7 +409,7 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) } // Patch fields if any - if ref.FromObject.FieldPath != nil { + if ref.PatchesFrom.FieldPath != nil { if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { return errors.Wrap(err, errPatchFromReferencedResource) } @@ -449,13 +478,14 @@ type refFinalizerFn func(context.Context, *unstructured.Unstructured, string) er 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 { + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(&ref) res := &unstructured.Unstructured{} - res.SetAPIVersion(ref.FromObject.APIVersion) - res.SetKind(ref.FromObject.Kind) + res.SetAPIVersion(refAPIVersion) + res.SetKind(refKind) // Try to get referenced resource err := f.client.Get(ctx, client.ObjectKey{ - Namespace: ref.FromObject.Namespace, - Name: ref.FromObject.Name, + Namespace: refNamespace, + Name: refName, }, res) if err != nil { diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 0b31f49d..67084a2e 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -133,13 +133,16 @@ func externalResourceWithLastAppliedConfigAnnotation(val interface{}) *unstructu } func objectReferences() []v1alpha1.Reference { + dependsOn := v1alpha1.DependsOn{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: v1alpha1.ObjectKind, + Name: testReferenceObjectName, + Namespace: testNamespace, + } ref := []v1alpha1.Reference{ { - FromObject: v1alpha1.FromObject{ - APIVersion: v1alpha1.SchemeGroupVersion.String(), - Kind: v1alpha1.ObjectKind, - Name: testReferenceObjectName, - Namespace: testNamespace, + PatchesFrom: v1alpha1.PatchesFrom{ + DependsOn: dependsOn, }, }, } @@ -647,7 +650,7 @@ func Test_helmExternal_Observe(t *testing.T) { args: args{ mg: kubernetesObject(func(obj *v1alpha1.Object) { obj.Spec.References = objectReferences() - obj.Spec.References[0].FromObject.FieldPath = pointer.StringPtr("nonexistent_field") + obj.Spec.References[0].PatchesFrom.FieldPath = pointer.StringPtr("nonexistent_field") }), client: resource.ClientApplicator{ Client: &test.MockClient{ diff --git a/package/crds/kubernetes.crossplane.io_objects.yaml b/package/crds/kubernetes.crossplane.io_objects.yaml index 6b84da7d..37386309 100644 --- a/package/crds/kubernetes.crossplane.io_objects.yaml +++ b/package/crds/kubernetes.crossplane.io_objects.yaml @@ -92,16 +92,38 @@ spec: items: description: Reference refers to an Object or arbitrary Kubernetes resource and optionally patch values from that resource to the current Object. properties: - fromObject: - description: FromObject is the reference of other Object or arbitrary Kubernetes resource + 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: @@ -111,15 +133,12 @@ spec: description: Namespace of the referenced object. type: string required: - - apiVersion - - kind + - 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 fromObject.fieldPath. + 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 - required: - - fromObject type: object type: array writeConnectionSecretToRef: From f4c30d59cfd921e070b3a22ad9a67d7fe0e10d94 Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Tue, 15 Feb 2022 16:37:37 +0800 Subject: [PATCH 07/11] Fix lint error Signed-off-by: Ying Mo --- internal/controller/object/object.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 0af291f3..4a3d16b2 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -358,7 +358,7 @@ func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) erro return nil } -func getReferenceInfo(ref *v1alpha1.Reference) (string, string, string, string) { +func getReferenceInfo(ref v1alpha1.Reference) (string, string, string, string) { // Get referenced resource APIVersion refAPIVersion := ref.DependsOn.APIVersion if ref.PatchesFrom.APIVersion != "" { @@ -394,7 +394,7 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) // Loop through references to resolve each referenced resource for _, ref := range obj.Spec.References { - refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(&ref) + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(ref) res := &unstructured.Unstructured{} res.SetAPIVersion(refAPIVersion) res.SetKind(refKind) @@ -478,7 +478,7 @@ type refFinalizerFn func(context.Context, *unstructured.Unstructured, string) er 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 { - refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(&ref) + refAPIVersion, refKind, refNamespace, refName := getReferenceInfo(ref) res := &unstructured.Unstructured{} res.SetAPIVersion(refAPIVersion) res.SetKind(refKind) From ccfa086a7d678ad2068a2501bcc1f2cf3949687e Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Tue, 15 Feb 2022 17:13:18 +0800 Subject: [PATCH 08/11] Append spec.forProvider.manifest. to toFieldPath Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index fc1f6c29..5b6c6cb5 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -181,7 +181,7 @@ func patchFieldValueToObject(path string, value interface{}, to runtime.Object) return err } - err = paved.SetValue(path, value) + err = paved.SetValue("spec.forProvider.manifest."+path, value) if err != nil { return err } From 6a2499bf0758bdc8c862380b253462aadad3560f Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Tue, 15 Feb 2022 17:51:36 +0800 Subject: [PATCH 09/11] Update kubernetes.crossplane.io_providerconfigs.yaml to reflect upstream change Signed-off-by: Ying Mo --- package/crds/kubernetes.crossplane.io_providerconfigs.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 19f1de73a36b408dc3dddd14a1901d6947a268f9 Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Wed, 16 Feb 2022 11:24:01 +0800 Subject: [PATCH 10/11] Refactor getReferenceInfo Signed-off-by: Ying Mo --- apis/object/v1alpha1/types.go | 6 ++- apis/object/v1alpha1/zz_generated.deepcopy.go | 12 ++++- internal/controller/object/object.go | 52 +++++++++---------- internal/controller/object/object_test.go | 36 ++++++++++++- 4 files changed, 75 insertions(+), 31 deletions(-) diff --git a/apis/object/v1alpha1/types.go b/apis/object/v1alpha1/types.go index 5b6c6cb5..2c4eda7c 100644 --- a/apis/object/v1alpha1/types.go +++ b/apis/object/v1alpha1/types.go @@ -84,10 +84,12 @@ type PatchesFrom struct { type Reference struct { // DependsOn is used to declare dependency on other Object or arbitrary // Kubernetes resource. - DependsOn `json:"dependsOn,omitempty"` + // +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. - PatchesFrom `json:"patchesFrom,omitempty"` + // +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. diff --git a/apis/object/v1alpha1/zz_generated.deepcopy.go b/apis/object/v1alpha1/zz_generated.deepcopy.go index 97d37204..0fb6f2f7 100644 --- a/apis/object/v1alpha1/zz_generated.deepcopy.go +++ b/apis/object/v1alpha1/zz_generated.deepcopy.go @@ -195,8 +195,16 @@ func (in *PatchesFrom) DeepCopy() *PatchesFrom { // 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 - out.DependsOn = in.DependsOn - in.PatchesFrom.DeepCopyInto(&out.PatchesFrom) + 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) diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 4a3d16b2..e3a5a869 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -359,31 +359,23 @@ func setObserved(obj *v1alpha1.Object, observed *unstructured.Unstructured) erro } func getReferenceInfo(ref v1alpha1.Reference) (string, string, string, string) { - // Get referenced resource APIVersion - refAPIVersion := ref.DependsOn.APIVersion - if ref.PatchesFrom.APIVersion != "" { - refAPIVersion = ref.PatchesFrom.APIVersion - } - - // Get referenced resource kind - refKind := ref.DependsOn.Kind - if ref.PatchesFrom.Kind != "" { - refKind = ref.PatchesFrom.Kind - } - - // Get referenced resource namespace - refNamespace := ref.DependsOn.Namespace - if ref.PatchesFrom.Namespace != "" { - refNamespace = ref.PatchesFrom.Namespace - } - - // Get referenced resource name - refName := ref.DependsOn.Name - if ref.PatchesFrom.Name != "" { - refName = ref.PatchesFrom.Name - } - - return refAPIVersion, refKind, refNamespace, refName + 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 @@ -394,6 +386,10 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) // 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) @@ -409,7 +405,7 @@ func (c *external) resolveReferencies(ctx context.Context, obj *v1alpha1.Object) } // Patch fields if any - if ref.PatchesFrom.FieldPath != nil { + if ref.PatchesFrom != nil && ref.PatchesFrom.FieldPath != nil { if err := ref.ApplyFromFieldPathPatch(res, obj); err != nil { return errors.Wrap(err, errPatchFromReferencedResource) } @@ -478,6 +474,10 @@ type refFinalizerFn func(context.Context, *unstructured.Unstructured, string) er 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) diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 67084a2e..12b6eb86 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -141,10 +141,13 @@ func objectReferences() []v1alpha1.Reference { } ref := []v1alpha1.Reference{ { - PatchesFrom: v1alpha1.PatchesFrom{ + PatchesFrom: &v1alpha1.PatchesFrom{ DependsOn: dependsOn, }, }, + { + DependsOn: &dependsOn, + }, } return ref } @@ -767,6 +770,22 @@ func Test_helmExternal_Observe(t *testing.T) { 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) { @@ -1177,6 +1196,21 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { 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) { From 46ef0d7e9c171b8fc23ff1762742dbb1491b3463 Mon Sep 17 00:00:00 2001 From: Ying Mo Date: Wed, 16 Feb 2022 11:24:58 +0800 Subject: [PATCH 11/11] Add examples Signed-off-by: Ying Mo --- examples/object/policy/default.yaml | 17 +++++++ .../object/policy/observe-create-update.yaml | 21 ++++++++ examples/object/policy/observe-delete.yaml | 26 ++++++++++ examples/object/policy/observe.yaml | 26 ++++++++++ .../object/references/depends-on-object.yaml | 39 +++++++++++++++ .../references/depends-on-resource.yaml | 31 ++++++++++++ .../patches-from-multiple-resources.yaml | 50 +++++++++++++++++++ .../references/patches-from-resource.yaml | 33 ++++++++++++ 8 files changed, 243 insertions(+) create mode 100644 examples/object/policy/default.yaml create mode 100644 examples/object/policy/observe-create-update.yaml create mode 100644 examples/object/policy/observe-delete.yaml create mode 100644 examples/object/policy/observe.yaml create mode 100644 examples/object/references/depends-on-object.yaml create mode 100644 examples/object/references/depends-on-resource.yaml create mode 100644 examples/object/references/patches-from-multiple-resources.yaml create mode 100644 examples/object/references/patches-from-resource.yaml 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