implement granular managementPolicies

Signed-off-by: lsviben <sviben.lovro@gmail.com>
This commit is contained in:
lsviben 2023-05-31 10:24:54 +02:00
parent e979e3c19d
commit 73a675c82c
No known key found for this signature in database
GPG Key ID: 2F79D7AE8BBBF450
9 changed files with 1124 additions and 199 deletions

View File

@ -16,23 +16,39 @@ limitations under the License.
package v1
// A ManagementPolicy determines how should Crossplane controllers manage an
// external resource.
// +kubebuilder:validation:Enum=FullControl;ObserveOnly;OrphanOnDelete
type ManagementPolicy string
// ManagementPolicies determine how should Crossplane controllers manage an
// external resource through an array of ManagementActions.
type ManagementPolicies []ManagementAction
// A ManagementAction represents an action that the Crossplane controllers
// can take on an external resource.
// +kubebuilder:validation:Enum=Observe;Create;Update;Delete;LateInitialize;*
type ManagementAction string
const (
// ManagementFullControl means the external resource is fully controlled
// by Crossplane controllers, including its deletion.
ManagementFullControl ManagementPolicy = "FullControl"
// ManagementActionObserve means that the managed resource status.atProvider
// will be updated with the external resource state.
ManagementActionObserve ManagementAction = "Observe"
// ManagementObserveOnly means the external resource will only be observed
// by Crossplane controllers, but not modified or deleted.
ManagementObserveOnly ManagementPolicy = "ObserveOnly"
// ManagementActionCreate means that the external resource will be created
// using the managed resource spec.initProvider and spec.forProvider.
ManagementActionCreate ManagementAction = "Create"
// ManagementOrphanOnDelete means the external resource will be orphaned
// when its managed resource is deleted.
ManagementOrphanOnDelete ManagementPolicy = "OrphanOnDelete"
// ManagementActionUpdate means that the external resource will be updated
// using the managed resource spec.forProvider.
ManagementActionUpdate ManagementAction = "Update"
// ManagementActionDelete means that the external resource will be deleted
// when the managed resource is deleted.
ManagementActionDelete ManagementAction = "Delete"
// ManagementActionLateInitialize means that unspecified fields of the managed
// resource spec.forProvider will be updated with the external resource state.
ManagementActionLateInitialize ManagementAction = "LateInitialize"
// ManagementActionAll means that all of the above actions will be taken
// by the Crossplane controllers.
ManagementActionAll ManagementAction = "*"
)
// A DeletionPolicy determines what should happen to the underlying external

View File

@ -205,20 +205,22 @@ type ResourceSpec struct {
// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored
// unless the relevant Crossplane feature flag is enabled, and may be
// changed or removed without notice.
// ManagementPolicy specifies the level of control Crossplane has over the
// managed external resource.
// ManagementPolicies specify the array of actions Crossplane is allowed to
// take on the managed and external resources.
// This field is planned to replace the DeletionPolicy field in a future
// release. Currently, both could be set independently and non-default
// values would be honored if the feature flag is enabled.
// values would be honored if the feature flag is enabled. If both are
// custom, the DeletionPolicy field will be ignored.
// See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223
// and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md
// +optional
// +kubebuilder:default=FullControl
ManagementPolicy ManagementPolicy `json:"managementPolicy,omitempty"`
// +kubebuilder:default={"*"}
ManagementPolicies ManagementPolicies `json:"managementPolicies,omitempty"`
// DeletionPolicy specifies what will happen to the underlying external
// when this managed resource is deleted - either "Delete" or "Orphan" the
// external resource.
// This field is planned to be deprecated in favor of the ManagementPolicy
// This field is planned to be deprecated in favor of the ManagementPolicies
// field in a future release. Currently, both could be set independently and
// non-default values would be honored if the feature flag is enabled.
// See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223

View File

@ -219,6 +219,25 @@ func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in ManagementPolicies) DeepCopyInto(out *ManagementPolicies) {
{
in := &in
*out = make(ManagementPolicies, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementPolicies.
func (in ManagementPolicies) DeepCopy() ManagementPolicies {
if in == nil {
return nil
}
out := new(ManagementPolicies)
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeOptions) DeepCopyInto(out *MergeOptions) {
*out = *in
@ -386,6 +405,11 @@ func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
*out = new(Reference)
(*in).DeepCopyInto(*out)
}
if in.ManagementPolicies != nil {
in, out := &in.ManagementPolicies, &out.ManagementPolicies
*out = make(ManagementPolicies, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSpec.

22
pkg/feature/features.go Normal file
View File

@ -0,0 +1,22 @@
/*
Copyright 2023 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package feature
// EnableAlphaManagementPolicies enables alpha support for
// Management Policies. See the below design for more details.
// https://github.com/crossplane/crossplane/pull/3531
const EnableAlphaManagementPolicies Flag = "EnableAlphaManagementPolicies"

View File

@ -0,0 +1,208 @@
/*
Copyright 2023 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package managed
import (
"fmt"
"k8s.io/apimachinery/pkg/util/sets"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
)
// ManagementPoliciesResolver is used to perform management policy checks
// based on the management policy and if the management policy feature is enabled.
type ManagementPoliciesResolver struct {
enabled bool
supportedPolicies []sets.Set[xpv1.ManagementAction]
managementPolicies sets.Set[xpv1.ManagementAction]
deletionPolicy xpv1.DeletionPolicy
}
// A ManagementPoliciesResolverOption configures a ManagementPoliciesResolver.
type ManagementPoliciesResolverOption func(*ManagementPoliciesResolver)
// WithSupportedManagementPolicies sets the supported management policies.
func WithSupportedManagementPolicies(supportedManagementPolicies []sets.Set[xpv1.ManagementAction]) ManagementPoliciesResolverOption {
return func(r *ManagementPoliciesResolver) {
r.supportedPolicies = supportedManagementPolicies
}
}
func defaultSupportedManagementPolicies() []sets.Set[xpv1.ManagementAction] {
return []sets.Set[xpv1.ManagementAction]{
// Default (all), the standard behaviour of crossplane in which all
// reconciler actions are done.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionAll),
// All actions explicitly set, the same as default.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate, xpv1.ManagementActionLateInitialize, xpv1.ManagementActionDelete),
// ObserveOnly, just observe action is done, the external resource is
// considered as read-only.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve),
// Pause, no action is being done. Alternative to setting the pause
// annotation.
sets.New[xpv1.ManagementAction](),
// No LateInitialize filling in the spec.forProvider, allowing some
// external resource fields to be managed externally.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate, xpv1.ManagementActionDelete),
// No Delete, the external resource is not deleted when the managed
// resource is deleted.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate, xpv1.ManagementActionLateInitialize),
// No Delete and no LateInitialize, the external resource is not deleted
// when the managed resource is deleted and the spec.forProvider is not
// late initialized.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionUpdate),
// No Update, the external resource is not updated when the managed
// resource is updated. Useful for immutable external resources.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete, xpv1.ManagementActionLateInitialize),
// No Update and no Delete, the external resource is not updated
// when the managed resource is updated and the external resource
// is not deleted when the managed resource is deleted.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionLateInitialize),
// No Update and no LateInitialize, the external resource is not updated
// when the managed resource is updated and the spec.forProvider is not
// late initialized.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete),
// No Update, no Delete and no LateInitialize, the external resource is
// not updated when the managed resource is updated, the external resource
// is not deleted when the managed resource is deleted and the
// spec.forProvider is not late initialized.
sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve, xpv1.ManagementActionCreate),
}
}
// NewManagementPoliciesResolver returns an ManagementPolicyChecker based
// on the management policies and if the management policies feature
// is enabled.
func NewManagementPoliciesResolver(managementPolicyEnabled bool, managementPolicy xpv1.ManagementPolicies, deletionPolicy xpv1.DeletionPolicy, o ...ManagementPoliciesResolverOption) ManagementPoliciesChecker {
r := &ManagementPoliciesResolver{
enabled: managementPolicyEnabled,
supportedPolicies: defaultSupportedManagementPolicies(),
managementPolicies: sets.New[xpv1.ManagementAction](managementPolicy...),
deletionPolicy: deletionPolicy,
}
for _, ro := range o {
ro(r)
}
return r
}
// Validate checks if the management policy is valid.
// If the management policy feature is disabled, but uses a non-default value,
// it returns an error.
// If the management policy feature is enabled, but uses a non-supported value,
// it returns an error.
func (m *ManagementPoliciesResolver) Validate() error {
// check if its disabled, but uses a non-default value.
if !m.enabled {
if !m.managementPolicies.Equal(sets.New[xpv1.ManagementAction](xpv1.ManagementActionAll)) && m.managementPolicies.Len() != 0 {
return fmt.Errorf(errFmtManagementPolicyNonDefault, m.managementPolicies.UnsortedList())
}
// if its just disabled we don't care about supported policies
return nil
}
// check if the policy is a non-supported combination
for _, p := range m.supportedPolicies {
if p.Equal(m.managementPolicies) {
return nil
}
}
return fmt.Errorf(errFmtManagementPolicyNotSupported, m.managementPolicies.UnsortedList())
}
// IsPaused returns true if the management policy is empty and the
// management policies feature is enabled
func (m *ManagementPoliciesResolver) IsPaused() bool {
if !m.enabled {
return false
}
return m.managementPolicies.Len() == 0
}
// ShouldCreate returns true if the Create action is allowed.
// If the management policy feature is disabled, it returns true.
func (m *ManagementPoliciesResolver) ShouldCreate() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionCreate, xpv1.ManagementActionAll)
}
// ShouldUpdate returns true if the Update action is allowed.
// If the management policy feature is disabled, it returns true.
func (m *ManagementPoliciesResolver) ShouldUpdate() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionUpdate, xpv1.ManagementActionAll)
}
// ShouldLateInitialize returns true if the LateInitialize action is allowed.
// If the management policy feature is disabled, it returns true.
func (m *ManagementPoliciesResolver) ShouldLateInitialize() bool {
if !m.enabled {
return true
}
return m.managementPolicies.HasAny(xpv1.ManagementActionLateInitialize, xpv1.ManagementActionAll)
}
// ShouldOnlyObserve returns true if the Observe action is allowed and all
// other actions are not allowed. If the management policy feature is disabled,
// it returns false.
func (m *ManagementPoliciesResolver) ShouldOnlyObserve() bool {
if !m.enabled {
return false
}
return m.managementPolicies.Equal(sets.New[xpv1.ManagementAction](xpv1.ManagementActionObserve))
}
// ShouldDelete returns true based on the combination of the deletionPolicy and
// the managementPolicies. If the management policy feature is disabled, it
// returns true if the deletionPolicy is set to "Delete". Otherwise, it checks
// which field is set to a non-default value and makes a decision based on that.
// We need to be careful until we completely remove the deletionPolicy in favor
// of managementPolicies which conflict with the deletionPolicy regarding
// deleting of the external resource. This function implements the proposal in
// the Ignore Changes design doc under the "Deprecation of `deletionPolicy`".
func (m *ManagementPoliciesResolver) ShouldDelete() bool {
if !m.enabled {
return m.deletionPolicy != xpv1.DeletionOrphan
}
// delete external resource if both the deletionPolicy and the
// managementPolicies are set to delete
if m.deletionPolicy == xpv1.DeletionDelete && m.managementPolicies.HasAny(xpv1.ManagementActionDelete, xpv1.ManagementActionAll) {
return true
}
// if the managementPolicies is not default, and it contains the deletion
// action, we should delete the external resource
if !m.managementPolicies.Equal(sets.New[xpv1.ManagementAction](xpv1.ManagementActionAll)) && m.managementPolicies.Has(xpv1.ManagementActionDelete) {
return true
}
// For all other cases, we should orphan the external resource.
// Obvious cases:
// DeletionOrphan && ManagementPolicies without Delete Action
// Conflicting cases:
// DeletionOrphan && Management Policy ["*"] (obeys non-default configuration)
// DeletionDelete && ManagementPolicies that does not include the Delete
// Action (obeys non-default configuration)
return false
}

View File

@ -22,6 +22,7 @@ import (
"time"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@ -29,6 +30,7 @@ import (
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/feature"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
@ -48,6 +50,9 @@ const (
// Error strings.
const (
errFmtManagementPolicyNonDefault = "`spec.managementPolicies` is set to a non-default value but the feature is not enabled: %s"
errFmtManagementPolicyNotSupported = "`spec.managementPolicies` is set to a value(%s) which is not supported. Check docs for supported policies"
errGetManaged = "cannot get managed resource"
errUpdateManagedAnnotations = "cannot update managed resource annotations"
errCreateIncomplete = "cannot determine creation result - remove the " + meta.AnnotationKeyExternalCreatePending + " annotation if it is safe to proceed"
@ -56,24 +61,24 @@ const (
errReconcileCreate = "create failed"
errReconcileUpdate = "update failed"
errReconcileDelete = "delete failed"
errManagementPolicy = "managementPolicy is set to a non-default value but the feature is not enabled."
errExternalResourceNotExist = "external resource does not exist"
)
// Event reasons.
const (
reasonCannotConnect event.Reason = "CannotConnectToProvider"
reasonCannotDisconnect event.Reason = "CannotDisconnectFromProvider"
reasonCannotInitialize event.Reason = "CannotInitializeManagedResource"
reasonCannotResolveRefs event.Reason = "CannotResolveResourceReferences"
reasonCannotObserve event.Reason = "CannotObserveExternalResource"
reasonCannotCreate event.Reason = "CannotCreateExternalResource"
reasonCannotDelete event.Reason = "CannotDeleteExternalResource"
reasonCannotPublish event.Reason = "CannotPublishConnectionDetails"
reasonCannotUnpublish event.Reason = "CannotUnpublishConnectionDetails"
reasonCannotUpdate event.Reason = "CannotUpdateExternalResource"
reasonCannotUpdateManaged event.Reason = "CannotUpdateManagedResource"
reasonManagementPolicyNotEnabled event.Reason = "CannotUseManagementPolicy"
reasonCannotConnect event.Reason = "CannotConnectToProvider"
reasonCannotDisconnect event.Reason = "CannotDisconnectFromProvider"
reasonCannotInitialize event.Reason = "CannotInitializeManagedResource"
reasonCannotResolveRefs event.Reason = "CannotResolveResourceReferences"
reasonCannotObserve event.Reason = "CannotObserveExternalResource"
reasonCannotCreate event.Reason = "CannotCreateExternalResource"
reasonCannotDelete event.Reason = "CannotDeleteExternalResource"
reasonCannotPublish event.Reason = "CannotPublishConnectionDetails"
reasonCannotUnpublish event.Reason = "CannotUnpublishConnectionDetails"
reasonCannotUpdate event.Reason = "CannotUpdateExternalResource"
reasonCannotUpdateManaged event.Reason = "CannotUpdateManagedResource"
reasonManagementPolicyInvalid event.Reason = "CannotUseInvalidManagementPolicy"
reasonDeleted event.Reason = "DeletedExternalResource"
reasonCreated event.Reason = "CreatedExternalResource"
@ -89,6 +94,29 @@ func ControllerName(kind string) string {
return "managed/" + strings.ToLower(kind)
}
// ManagementPoliciesChecker is used to perform checks on management policies
// to determine specific actions are allowed, or if they are the only allowed
// action.
type ManagementPoliciesChecker interface {
// Validate validates the management policies.
Validate() error
// IsPaused returns true if the resource is paused based
// on the management policy.
IsPaused() bool
// ShouldOnlyObserve returns true if only the Observe action is allowed.
ShouldOnlyObserve() bool
// ShouldCreate returns true if the Create action is allowed.
ShouldCreate() bool
// ShouldLateInitialize returns true if the LateInitialize action is
// allowed.
ShouldLateInitialize() bool
// ShouldUpdate returns true if the Update action is allowed.
ShouldUpdate() bool
// ShouldDelete returns true if the Delete action is allowed.
ShouldDelete() bool
}
// A CriticalAnnotationUpdater is used when it is critical that annotations must
// be updated before returning from the Reconcile loop.
type CriticalAnnotationUpdater interface {
@ -448,10 +476,11 @@ type Reconciler struct {
client client.Client
newManaged func() resource.Managed
pollInterval time.Duration
timeout time.Duration
creationGracePeriod time.Duration
managementPoliciesEnabled bool
pollInterval time.Duration
timeout time.Duration
creationGracePeriod time.Duration
features feature.Flags
// The below structs embed the set of interfaces used to implement the
// managed resource reconciler. We do this primarily for readability, so
@ -460,6 +489,8 @@ type Reconciler struct {
external mrExternal
managed mrManaged
supportedManagementPolicies []sets.Set[xpv1.ManagementAction]
log logging.Logger
record event.Recorder
}
@ -605,7 +636,15 @@ func WithRecorder(er event.Recorder) ReconcilerOption {
// WithManagementPolicies enables support for management policies.
func WithManagementPolicies() ReconcilerOption {
return func(r *Reconciler) {
r.managementPoliciesEnabled = true
r.features.Enable(feature.EnableAlphaManagementPolicies)
}
}
// WithReconcilerSupportedManagementPolicies configures which management policies are
// supported by the reconciler.
func WithReconcilerSupportedManagementPolicies(supported []sets.Set[xpv1.ManagementAction]) ReconcilerOption {
return func(r *Reconciler) {
r.supportedManagementPolicies = supported
}
}
@ -626,15 +665,16 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp
_ = nm()
r := &Reconciler{
client: m.GetClient(),
newManaged: nm,
pollInterval: defaultpollInterval,
creationGracePeriod: defaultGracePeriod,
timeout: reconcileTimeout,
managed: defaultMRManaged(m),
external: defaultMRExternal(),
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
client: m.GetClient(),
newManaged: nm,
pollInterval: defaultpollInterval,
creationGracePeriod: defaultGracePeriod,
timeout: reconcileTimeout,
managed: defaultMRManaged(m),
external: defaultMRExternal(),
supportedManagementPolicies: defaultSupportedManagementPolicies(),
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
}
for _, ro := range o {
@ -675,35 +715,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
"external-name", meta.GetExternalName(managed),
)
// Check the pause annotation and return if it has the value "true"
// after logging, publishing an event and updating the SYNC status condition
if meta.IsPaused(managed) {
log.Debug("Reconciliation is paused via the pause annotation", "annotation", meta.AnnotationKeyReconciliationPaused, "value", "true")
record.Event(managed, event.Normal(reasonReconciliationPaused, "Reconciliation is paused via the pause annotation"))
managementPoliciesEnabled := r.features.Enabled(feature.EnableAlphaManagementPolicies)
if managementPoliciesEnabled {
log.WithValues("managementPolicies", managed.GetManagementPolicies())
}
// Create the management policy resolver which will assist us in determining
// what actions to take on the managed resource based on the management
// and deletion policies.
policy := NewManagementPoliciesResolver(managementPoliciesEnabled, managed.GetManagementPolicies(), managed.GetDeletionPolicy(), WithSupportedManagementPolicies(r.supportedManagementPolicies))
// Check if the resource has paused reconciliation based on the
// annotation or the management policies.
// Log, publish an event and update the SYNC status condition.
if meta.IsPaused(managed) || policy.IsPaused() {
log.Debug("Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation", "annotation", meta.AnnotationKeyReconciliationPaused)
record.Event(managed, event.Normal(reasonReconciliationPaused, "Reconciliation is paused either through the `spec.managementPolicies` or the pause annotation",
"annotation", meta.AnnotationKeyReconciliationPaused))
managed.SetConditions(xpv1.ReconcilePaused())
// if the pause annotation is removed, we will have a chance to reconcile again and resume
// and if status update fails, we will reconcile again to retry to update the status
// if the pause annotation is removed or the management policies changed, we will have a chance to reconcile
// again and resume and if status update fails, we will reconcile again to retry to update the status
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// Check if the ManagementPolicy is set to a non-default value while the
// Check if the ManagementPolicies is set to a non-default value while the
// feature is not enabled. This is a safety check to let users know that
// they need to enable the feature flag before using the feature. For
// example, we wouldn't want someone to set the policy to ObserveOnly but
// not realize that the controller is still trying to reconcile
// (and modify or delete) the resource since they forgot to enable the
// feature flag.
if !r.managementPoliciesEnabled && (managed.GetManagementPolicy() == xpv1.ManagementObserveOnly || managed.GetManagementPolicy() == xpv1.ManagementOrphanOnDelete) {
log.Debug(errManagementPolicy, "policy", managed.GetManagementPolicy())
record.Event(managed, event.Warning(reasonManagementPolicyNotEnabled, errors.New(errManagementPolicy)))
managed.SetConditions(xpv1.ReconcileError(errors.New(errManagementPolicy)))
// feature flag. Also checks if the management policy is set to a value
// that is not supported by the controller.
if err := policy.Validate(); err != nil {
log.Debug(err.Error())
record.Event(managed, event.Warning(reasonManagementPolicyInvalid, err))
managed.SetConditions(xpv1.ReconcileError(err))
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// If managed resource has a deletion timestamp and a deletion policy of
// Orphan, we do not need to observe the external resource before attempting
// to unpublish connection details and remove finalizer.
if meta.WasDeleted(managed) && shouldOrphan(r.managementPoliciesEnabled, managed) {
if meta.WasDeleted(managed) && !policy.ShouldDelete() {
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
// Empty ConnectionDetails are passed to UnpublishConnection because we
@ -816,41 +869,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
if r.managementPoliciesEnabled && managed.GetManagementPolicy() == xpv1.ManagementObserveOnly {
// In the observe-only mode, !observation.ResourceExists will be an error
// case, and we will explicitly return this information to the user.
if !observation.ResourceExists {
record.Event(managed, event.Warning(reasonCannotObserve, errors.New(errExternalResourceNotExist)))
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// It is a valid use case to Observe a resource to get its connection
// details, so we publish them here.
if _, err := r.managed.PublishConnection(ctx, managed, observation.ConnectionDetails); err != nil {
// If this is the first time we encounter this issue we'll be
// requeued implicitly when we update our status with the new error
// condition. If not, we requeue explicitly, which will trigger
// backoff.
log.Debug("Cannot publish connection details", "error", err)
record.Event(managed, event.Warning(reasonCannotPublish, err))
managed.SetConditions(xpv1.ReconcileError(err))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// Since we're in the ObserveOnly mode, we don't want to update the spec
// of the managed resource for any reason including the late
// initialization of fields. So, we ignore `observation.ResourceLateInitialized`
// and do not make an `Update` call on the managed resource ensuring any
// spec change is ignored.
// We are returning a ReconcileSuccess here because we have observed the
// resource successfully, and we don't need any further action in this
// reconcile.
log.Debug("Observed the resource successfully with management policy ObserveOnly", "requeue-after", time.Now().Add(r.pollInterval))
managed.SetConditions(xpv1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
// In the observe-only mode, !observation.ResourceExists will be an error
// case, and we will explicitly return this information to the user.
if !observation.ResourceExists && policy.ShouldOnlyObserve() {
record.Event(managed, event.Warning(reasonCannotObserve, errors.New(errExternalResourceNotExist)))
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)))
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// If this resource has a non-zero creation grace period we want to wait
// for that period to expire before we trust that the resource really
// doesn't exist. This is because some external APIs are eventually
@ -865,9 +891,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
if meta.WasDeleted(managed) {
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
// We'll only reach this point if deletion policy is not orphan, so we
// are safe to call external deletion if external resource exists.
if observation.ResourceExists {
if observation.ResourceExists && policy.ShouldDelete() {
if err := external.Delete(externalCtx, managed); err != nil {
// We'll hit this condition if we can't delete our external
// resource, for example if our provider credentials don't have
@ -940,7 +964,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
if !observation.ResourceExists {
if !observation.ResourceExists && policy.ShouldCreate() {
// We write this annotation for two reasons. Firstly, it helps
// us to detect the case in which we fail to persist critical
// information (like the external name) that may be set by the
@ -1030,7 +1054,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
if observation.ResourceLateInitialized {
if observation.ResourceLateInitialized && policy.ShouldLateInitialize() {
// Note that this update may reset any pending updates to the status of
// the managed resource from when it was observed above. This is because
// the API server replies to the update with its unchanged view of the
@ -1062,6 +1086,13 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
log.Debug("External resource differs from desired state", "diff", observation.Diff)
}
// skip the update if the management policy is set to ignore updates
if !policy.ShouldUpdate() {
log.Debug("Skipping update due to managementPolicies. Reconciliation succeeded", "requeue-after", time.Now().Add(r.pollInterval))
managed.SetConditions(xpv1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
update, err := external.Update(externalCtx, managed)
if err != nil {
// We'll hit this condition if we can't update our external resource,
@ -1095,29 +1126,3 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
managed.SetConditions(xpv1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// We need to be careful until we completely remove the deletionPolicy in favor
// of managementPolicies which conflicts with the managementPolicy regarding
// orphaning of the external resource. This function implement the proposal in
// the Observe Only design doc under the "Deprecation of `deletionPolicy`"
// section by triggering external resource deletion only when the deletionPolicy
// is set to "Delete" and the managementPolicy is set to "FullControl".
func shouldOrphan(managementPoliciesEnabled bool, managed resource.Managed) bool {
if !managementPoliciesEnabled {
return managed.GetDeletionPolicy() == xpv1.DeletionOrphan
}
if managed.GetDeletionPolicy() == xpv1.DeletionDelete && managed.GetManagementPolicy() == xpv1.ManagementFullControl {
// This is the only case where we should delete the external resource,
// so do not orphan it.
return false
}
// For all other cases, we should orphan the external resource.
// Obvious cases:
// DeletionOrphan && ManagementOrphanOnDelete
// DeletionOrphan && ManagementObserveOnly
// Conflicting cases:
// DeletionOrphan && ManagementFullControl (obeys non-default configuration)
// DeletionDelete && ManagementObserveOnly (obeys non-default configuration)
// DeletionDelete && ManagementOrphanOnDelete (obeys non-default configuration)
return true
}

View File

@ -18,6 +18,7 @@ package managed
import (
"context"
"fmt"
"testing"
"time"
@ -26,6 +27,7 @@ import (
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@ -1104,6 +1106,39 @@ func TestReconciler(t *testing.T) {
},
want: want{result: reconcile.Result{}},
},
"ManagementPolicyReconciliationPausedSuccessful": {
reason: `If a managed resource has the pause annotation with value "true", there should be no further requeue requests.`,
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{})
want.SetConditions(xpv1.ReconcilePaused())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := `If managed resource has the pause annotation with value "true", it should acquire "Synced" status condition with the status "False" and the reason "ReconcilePaused".`
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagementPolicies(),
WithInitializers(),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{}},
},
"ReconciliationResumes": {
reason: `If a managed resource has the pause annotation with some value other than "true" and the Synced=False/ReconcilePaused status condition, reconciliation should resume with requeueing.`,
args: args{
@ -1176,13 +1211,13 @@ func TestReconciler(t *testing.T) {
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicy(xpv1.ManagementObserveOnly)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionCreate})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicy(xpv1.ManagementObserveOnly)
want.SetConditions(xpv1.ReconcileError(errors.New(errManagementPolicy)))
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionCreate})
want.SetConditions(xpv1.ReconcileError(fmt.Errorf(errFmtManagementPolicyNonDefault, xpv1.ManagementPolicies{xpv1.ManagementActionCreate})))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := `If managed resource has a non default management policy but feature not enabled, it should return a proper error.`
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
@ -1196,19 +1231,80 @@ func TestReconciler(t *testing.T) {
},
want: want{result: reconcile.Result{}},
},
"ObserveOnlyResourceDoesNotExist": {
reason: "With ObserveOnly, observing a resource that does not exist should be reported as a conditioned status error.",
"ManagementPoliciyNotSupported": {
reason: `If an unsupported management policy is used, we should throw an error.`,
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicy(xpv1.ManagementObserveOnly)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionCreate})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicy(xpv1.ManagementObserveOnly)
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionCreate})
want.SetConditions(xpv1.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv1.ManagementPolicies{xpv1.ManagementActionCreate})))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := `If managed resource has non supported management policy, it should return a proper error.`
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagementPolicies(),
},
},
want: want{result: reconcile.Result{}},
},
"CustomManagementPoliciyNotSupported": {
reason: `If a custom unsupported management policy is used, we should throw an error.`,
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
want.SetConditions(xpv1.ReconcileError(fmt.Errorf(errFmtManagementPolicyNotSupported, xpv1.ManagementPolicies{xpv1.ManagementActionAll})))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := `If managed resource has non supported management policy, it should return a proper error.`
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagementPolicies(),
WithReconcilerSupportedManagementPolicies([]sets.Set[xpv1.ManagementAction]{sets.New(xpv1.ManagementActionObserve)}),
},
},
want: want{result: reconcile.Result{}},
},
"ObserveOnlyResourceDoesNotExist": {
reason: "With only Observe management action, observing a resource that does not exist should be reported as a conditioned status error.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errors.New(errExternalResourceNotExist), errReconcileObserve)))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "Resource does not exist should be reported as a conditioned status when ObserveOnly."
@ -1236,18 +1332,18 @@ func TestReconciler(t *testing.T) {
want: want{result: reconcile.Result{Requeue: true}},
},
"ObserveOnlyPublishConnectionDetailsError": {
reason: "With ObserveOnly, errors publishing connection details after observation should trigger a requeue after a short wait.",
reason: "With Observe, errors publishing connection details after observation should trigger a requeue after a short wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicy(xpv1.ManagementObserveOnly)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicy(xpv1.ManagementObserveOnly)
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
want.SetConditions(xpv1.ReconcileError(errBoom))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "Errors publishing connection details after observation should be reported as a conditioned status."
@ -1280,18 +1376,18 @@ func TestReconciler(t *testing.T) {
want: want{result: reconcile.Result{Requeue: true}},
},
"ObserveOnlySuccessfulObserve": {
reason: "With ObserveOnly, a successful managed resource observe should trigger a requeue after a long wait.",
reason: "With Observe, a successful managed resource observe should trigger a requeue after a long wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicy(xpv1.ManagementObserveOnly)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicy(xpv1.ManagementObserveOnly)
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve})
want.SetConditions(xpv1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "With ObserveOnly, a successful managed resource observation should be reported as a conditioned status."
@ -1319,6 +1415,266 @@ func TestReconciler(t *testing.T) {
return false, nil
},
}),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}},
},
"ManagementPolicyAllCreateSuccessful": {
reason: "Successful managed resource creation using management policy all should trigger a requeue after a short wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
meta.SetExternalCreatePending(want, time.Now())
meta.SetExternalCreateSucceeded(want, time.Now())
want.SetConditions(xpv1.ReconcileSuccess())
want.SetConditions(xpv1.Creating())
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
reason := "Successful managed resource creation should be reported as a conditioned status."
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(&NopConnecter{}),
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: true}},
},
"ManagementPolicyCreateCreateSuccessful": {
reason: "Successful managed resource creation using management policy Create should trigger a requeue after a short wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
meta.SetExternalCreatePending(want, time.Now())
meta.SetExternalCreateSucceeded(want, time.Now())
want.SetConditions(xpv1.ReconcileSuccess())
want.SetConditions(xpv1.Creating())
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
reason := "Successful managed resource creation should be reported as a conditioned status."
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(&NopConnecter{}),
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: true}},
},
"ManagementPolicyImmutable": {
reason: "Successful reconciliation skipping update should trigger a requeue after a long wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionLateInitialize, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete})
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionLateInitialize, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete})
want.SetConditions(xpv1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := `Managed resource should acquire Synced=False/ReconcileSuccess status condition.`
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
c := &ExternalClientFns{
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil
},
UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) {
return ExternalUpdate{}, errBoom
},
}
return c, nil
})),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}},
},
"ManagementPolicyAllUpdateSuccessful": {
reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
want.SetConditions(xpv1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "A successful managed resource update should be reported as a conditioned status."
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
c := &ExternalClientFns{
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil
},
UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) {
return ExternalUpdate{}, nil
},
}
return c, nil
})),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}},
},
"ManagementPolicyUpdateUpdateSuccessful": {
reason: "A successful managed resource update using management policies should trigger a requeue after a long wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
return nil
}),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionAll})
want.SetConditions(xpv1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "A successful managed resource update should be reported as a conditioned status."
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
c := &ExternalClientFns{
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
return ExternalObservation{ResourceExists: true, ResourceUpToDate: false}, nil
},
UpdateFn: func(_ context.Context, _ resource.Managed) (ExternalUpdate, error) {
return ExternalUpdate{}, nil
},
}
return c, nil
})),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}},
},
"ManagementPolicySkipLateInitialize": {
reason: "Should skip updating a managed resource to persist late initialized fields and should trigger a requeue after a long wait.",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
mg := obj.(*fake.Managed)
mg.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionUpdate, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete})
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
MockStatusUpdate: test.MockSubResourceUpdateFn(func(_ context.Context, obj client.Object, _ ...client.SubResourceUpdateOption) error {
want := &fake.Managed{}
want.SetManagementPolicies(xpv1.ManagementPolicies{xpv1.ManagementActionObserve, xpv1.ManagementActionUpdate, xpv1.ManagementActionCreate, xpv1.ManagementActionDelete})
want.SetConditions(xpv1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "Errors updating a managed resource should be reported as a conditioned status."
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Managed{}),
},
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithInitializers(),
WithManagementPolicies(),
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
c := &ExternalClientFns{
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
return ExternalObservation{ResourceExists: true, ResourceUpToDate: true, ResourceLateInitialized: true}, nil
},
}
return c, nil
})),
WithConnectionPublishers(),
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: defaultpollInterval}},
@ -1341,13 +1697,319 @@ func TestReconciler(t *testing.T) {
}
}
func TestShouldOrphan(t *testing.T) {
func TestTestManagementPoliciesResolverIsPaused(t *testing.T) {
type args struct {
enabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want bool
}{
"Disabled": {
reason: "Should return false if management policies are disabled",
args: args{
enabled: false,
policy: xpv1.ManagementPolicies{},
},
want: false,
},
"EnabledEmptyPolicies": {
reason: "Should return true if the management policies are enabled and empty",
args: args{
enabled: true,
policy: xpv1.ManagementPolicies{},
},
want: true,
},
"EnabledNonEmptyPolicies": {
reason: "Should return true if the management policies are enabled and non empty",
args: args{
enabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.enabled, tc.args.policy, xpv1.DeletionDelete)
if diff := cmp.Diff(tc.want, r.IsPaused()); diff != "" {
t.Errorf("\nReason: %s\nIsPaused(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestManagementPoliciesResolverValidate(t *testing.T) {
type args struct {
enabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want error
}{
"Enabled": {
reason: "Should return nil if the management policy is enabled.",
args: args{
enabled: true,
policy: xpv1.ManagementPolicies{},
},
want: nil,
},
"DisabledNonDefault": {
reason: "Should return error if the management policy is non-default and disabled.",
args: args{
enabled: false,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionCreate},
},
want: fmt.Errorf(errFmtManagementPolicyNonDefault, []xpv1.ManagementAction{xpv1.ManagementActionCreate}),
},
"DisabledDefault": {
reason: "Should return nil if the management policy is default and disabled.",
args: args{
enabled: false,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: nil,
},
"EnabledSupported": {
reason: "Should return nil if the management policy is supported.",
args: args{
enabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: nil,
},
"EnabledNotSupported": {
reason: "Should return err if the management policy is not supported.",
args: args{
enabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionDelete},
},
want: fmt.Errorf(errFmtManagementPolicyNotSupported, []xpv1.ManagementAction{xpv1.ManagementActionDelete}),
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.enabled, tc.args.policy, xpv1.DeletionDelete)
if diff := cmp.Diff(tc.want, r.Validate(), test.EquateErrors()); diff != "" {
t.Errorf("\nReason: %s\nIsNonDefault(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestManagementPoliciesResolverShouldCreate(t *testing.T) {
type args struct {
managementPoliciesEnabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want bool
}{
"ManagementPoliciesDisabled": {
reason: "Should return true if management policies are disabled",
args: args{
managementPoliciesEnabled: false,
},
want: true,
},
"ManagementPoliciesEnabledHasCreate": {
reason: "Should return true if management policies are enabled and managementPolicies has action Create",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionCreate},
},
want: true,
},
"ManagementPoliciesEnabledHasCreateAll": {
reason: "Should return true if management policies are enabled and managementPolicies has action All",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: true,
},
"ManagementPoliciesEnabledActionNotAllowed": {
reason: "Should return false if management policies are enabled and managementPolicies does not have Create",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve},
},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv1.DeletionOrphan)
if diff := cmp.Diff(tc.want, r.ShouldCreate()); diff != "" {
t.Errorf("\nReason: %s\nShouldCreate(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestManagementPoliciesResolverShouldUpdate(t *testing.T) {
type args struct {
managementPoliciesEnabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want bool
}{
"ManagementPoliciesDisabled": {
reason: "Should return true if management policies are disabled",
args: args{
managementPoliciesEnabled: false,
},
want: true,
},
"ManagementPoliciesEnabledHasUpdate": {
reason: "Should return true if management policies are enabled and managementPolicies has action Update",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionUpdate},
},
want: true,
},
"ManagementPoliciesEnabledHasUpdateAll": {
reason: "Should return true if management policies are enabled and managementPolicies has action All",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: true,
},
"ManagementPoliciesEnabledActionNotAllowed": {
reason: "Should return false if management policies are enabled and managementPolicies does not have Update",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve},
},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv1.DeletionOrphan)
if diff := cmp.Diff(tc.want, r.ShouldUpdate()); diff != "" {
t.Errorf("\nReason: %s\nShouldUpdate(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestManagementPoliciesResolverShouldLateInitialize(t *testing.T) {
type args struct {
managementPoliciesEnabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want bool
}{
"ManagementPoliciesDisabled": {
reason: "Should return true if management policies are disabled",
args: args{
managementPoliciesEnabled: false,
},
want: true,
},
"ManagementPoliciesEnabledHasLateInitialize": {
reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionLateInitialize},
},
want: true,
},
"ManagementPoliciesEnabledHasLateInitializeAll": {
reason: "Should return true if management policies are enabled and managementPolicies has action All",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
want: true,
},
"ManagementPoliciesEnabledActionNotAllowed": {
reason: "Should return false if management policies are enabled and managementPolicies does not have LateInitialize",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve},
},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv1.DeletionOrphan)
if diff := cmp.Diff(tc.want, r.ShouldLateInitialize()); diff != "" {
t.Errorf("\nReason: %s\nShouldLateInitialize(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestManagementPoliciesResolverOnlyObserve(t *testing.T) {
type args struct {
managementPoliciesEnabled bool
policy xpv1.ManagementPolicies
}
cases := map[string]struct {
reason string
args args
want bool
}{
"ManagementPoliciesDisabled": {
reason: "Should return false if management policies are disabled",
args: args{
managementPoliciesEnabled: false,
},
want: false,
},
"ManagementPoliciesEnabledHasOnlyObserve": {
reason: "Should return true if management policies are enabled and managementPolicies has action LateInitialize",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve},
},
want: true,
},
"ManagementPoliciesEnabledHasMultipleActions": {
reason: "Should return false if management policies are enabled and managementPolicies has multiple actions",
args: args{
managementPoliciesEnabled: true,
policy: xpv1.ManagementPolicies{xpv1.ManagementActionLateInitialize, xpv1.ManagementActionObserve},
},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.policy, xpv1.DeletionOrphan)
if diff := cmp.Diff(tc.want, r.ShouldOnlyObserve()); diff != "" {
t.Errorf("\nReason: %s\nShouldOnlyObserve(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestShouldDelete(t *testing.T) {
type args struct {
managementPoliciesEnabled bool
managed resource.Managed
}
type want struct {
orphan bool
delete bool
}
cases := map[string]struct {
reason string
@ -1364,10 +2026,10 @@ func TestShouldOrphan(t *testing.T) {
},
},
},
want: want{orphan: true},
want: want{delete: false},
},
"DeletionDelete": {
reason: "Should not orphan if management policies are disabled and deletion policy is set to Delete.",
reason: "Should delete if management policies are disabled and deletion policy is set to Delete.",
args: args{
managementPoliciesEnabled: false,
managed: &fake.Managed{
@ -1376,10 +2038,10 @@ func TestShouldOrphan(t *testing.T) {
},
},
},
want: want{orphan: false},
want: want{delete: true},
},
"DeletionDeleteManagementFullControl": {
reason: "Should not orphan if management policies are enabled and deletion policy is set to Delete and management policy is set to FullControl.",
"DeletionDeleteManagementActionAll": {
reason: "Should delete if management policies are enabled and deletion policy is set to Delete and management policy is set to All.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
@ -1387,14 +2049,14 @@ func TestShouldOrphan(t *testing.T) {
Policy: xpv1.DeletionDelete,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementFullControl,
Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
},
},
want: want{orphan: false},
want: want{delete: true},
},
"DeletionOrphanManagementOrphanOnDelete": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Orphan and management policy is set to OrphanOnDelete.",
"DeletionOrphanManagementActionAll": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Orphan and management policy is set to All.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
@ -1402,44 +2064,14 @@ func TestShouldOrphan(t *testing.T) {
Policy: xpv1.DeletionOrphan,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementOrphanOnDelete,
Policy: xpv1.ManagementPolicies{xpv1.ManagementActionAll},
},
},
},
want: want{orphan: true},
want: want{delete: false},
},
"DeletionOrphanManagementObserveOnly": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Orphan and management policy is set to ObserveOnly.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
Orphanable: fake.Orphanable{
Policy: xpv1.DeletionOrphan,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementObserveOnly,
},
},
},
want: want{orphan: true},
},
"DeletionOrphanManagementFullControl": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Orphan and management policy is set to FullControl.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
Orphanable: fake.Orphanable{
Policy: xpv1.DeletionOrphan,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementFullControl,
},
},
},
want: want{orphan: true},
},
"DeletionDeleteManagementObserveOnly": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Delete and management policy is set to ObserveOnly.",
"DeletionDeleteManagementActionDelete": {
reason: "Should delete if management policies are enabled and deletion policy is set to Delete and management policy has action Delete.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
@ -1447,14 +2079,29 @@ func TestShouldOrphan(t *testing.T) {
Policy: xpv1.DeletionDelete,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementObserveOnly,
Policy: xpv1.ManagementPolicies{xpv1.ManagementActionDelete},
},
},
},
want: want{orphan: true},
want: want{delete: true},
},
"DeletionDeleteManagementOrphanOnDelete": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Delete and management policy is set to OrphanOnDelete.",
"DeletionOrphanManagementActionDelete": {
reason: "Should delete if management policies are enabled and deletion policy is set to Orphan and management policy has action Delete.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
Orphanable: fake.Orphanable{
Policy: xpv1.DeletionOrphan,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementPolicies{xpv1.ManagementActionDelete},
},
},
},
want: want{delete: true},
},
"DeletionDeleteManagementActionNoDelete": {
reason: "Should orphan if management policies are enabled and deletion policy is set to Delete and management policy does not have action Delete.",
args: args{
managementPoliciesEnabled: true,
managed: &fake.Managed{
@ -1462,17 +2109,18 @@ func TestShouldOrphan(t *testing.T) {
Policy: xpv1.DeletionDelete,
},
Manageable: fake.Manageable{
Policy: xpv1.ManagementOrphanOnDelete,
Policy: xpv1.ManagementPolicies{xpv1.ManagementActionObserve},
},
},
},
want: want{orphan: true},
want: want{delete: false},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
if diff := cmp.Diff(tc.want.orphan, shouldOrphan(tc.args.managementPoliciesEnabled, tc.args.managed)); diff != "" {
t.Errorf("\nReason: %s\nshouldOrphan(...): -want, +got:\n%s", tc.reason, diff)
r := NewManagementPoliciesResolver(tc.args.managementPoliciesEnabled, tc.args.managed.GetManagementPolicies(), tc.args.managed.GetDeletionPolicy())
if diff := cmp.Diff(tc.want.delete, r.ShouldDelete()); diff != "" {
t.Errorf("\nReason: %s\nShouldDelete(...): -want, +got:\n%s", tc.reason, diff)
}
})
}

View File

@ -153,13 +153,13 @@ func (m *ConnectionDetailsPublisherTo) GetPublishConnectionDetailsTo() *xpv1.Pub
}
// Manageable implements the Manageable interface.
type Manageable struct{ Policy xpv1.ManagementPolicy }
type Manageable struct{ Policy xpv1.ManagementPolicies }
// SetManagementPolicy sets the ManagementPolicy.
func (m *Manageable) SetManagementPolicy(p xpv1.ManagementPolicy) { m.Policy = p }
// SetManagementPolicies sets the ManagementPolicies.
func (m *Manageable) SetManagementPolicies(p xpv1.ManagementPolicies) { m.Policy = p }
// GetManagementPolicy gets the ManagementPolicy.
func (m *Manageable) GetManagementPolicy() xpv1.ManagementPolicy { return m.Policy }
// GetManagementPolicies gets the ManagementPolicies.
func (m *Manageable) GetManagementPolicies() xpv1.ManagementPolicies { return m.Policy }
// Orphanable implements the Orphanable interface.
type Orphanable struct{ Policy xpv1.DeletionPolicy }

View File

@ -67,10 +67,10 @@ type ConnectionDetailsPublisherTo interface {
GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo
}
// A Manageable resource may specify a ManagementPolicy.
// A Manageable resource may specify a ManagementPolicies.
type Manageable interface {
SetManagementPolicy(p xpv1.ManagementPolicy)
GetManagementPolicy() xpv1.ManagementPolicy
SetManagementPolicies(p xpv1.ManagementPolicies)
GetManagementPolicies() xpv1.ManagementPolicies
}
// An Orphanable resource may specify a DeletionPolicy.