Skip to content

Commit

Permalink
implement granular managementPolicies
Browse files Browse the repository at this point in the history
Signed-off-by: lsviben <sviben.lovro@gmail.com>
  • Loading branch information
lsviben committed Jun 20, 2023
1 parent 85710e3 commit ab78f63
Show file tree
Hide file tree
Showing 6 changed files with 528 additions and 115 deletions.
42 changes: 29 additions & 13 deletions apis/common/v1/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ManagementPolicy determines how should Crossplane controllers manage an
// external resource through an array of ManagementActions.
type ManagementPolicy []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"

// ManagementActionCreate means that the external resource will be created
// when using the managed resource spec.initProvider and spec.forProvider.
ManagementActionCreate ManagementAction = "Create"

// ManagementActionUpdate means that the external resource will be updated
// when 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"

// ManagementObserveOnly means the external resource will only be observed
// by Crossplane controllers, but not modified or deleted.
ManagementObserveOnly ManagementPolicy = "ObserveOnly"
// ManagementActionLateInitialize means that unspecified fields of the managed
// resource spec.forProvider will be updated with the external resource state.
ManagementActionLateInitialize ManagementAction = "LateInitialize"

// ManagementOrphanOnDelete means the external resource will be orphaned
// when its managed resource is deleted.
ManagementOrphanOnDelete ManagementPolicy = "OrphanOnDelete"
// 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
Expand Down
8 changes: 5 additions & 3 deletions apis/common/v1/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,16 @@ 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.
// ManagementPolicy specifies 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.
// See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223
// This field is replacing the ManagementPolicy alpha field.
// Only the ManagementPolicies field will be honored.
// +optional
// +kubebuilder:default=FullControl
// +kubebuilder:default={"*"}
ManagementPolicy ManagementPolicy `json:"managementPolicy,omitempty"`

// DeletionPolicy specifies what will happen to the underlying external
Expand Down
24 changes: 24 additions & 0 deletions apis/common/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/meta/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ func AllowsPropagationTo(from metav1.Object) map[types.NamespacedName]bool {

// IsPaused returns true if the object has the AnnotationKeyReconciliationPaused
// annotation set to `true`.
// Deprecated: Use reconciler.IsPaused instead to check if the Crossplane managed resource is paused as it includes
// the check for management policies.
func IsPaused(o metav1.Object) bool {
return o.GetAnnotations()[AnnotationKeyReconciliationPaused] == "true"
}
118 changes: 62 additions & 56 deletions pkg/reconciler/managed/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,13 +676,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
)

// 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"))
// or if the management policies are enabled and the ManagementPolicy is empty.
// Log, publish an event and update the SYNC status condition.
if IsPaused(managed, r.managementPoliciesEnabled) {
log.Debug("Reconciliation is paused through the management policy or the pause annotation")
record.Event(managed, event.Normal(reasonReconciliationPaused, "Reconciliation is paused"))
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)
}

Expand All @@ -693,7 +694,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
// 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) {
if !r.managementPoliciesEnabled && (managed.GetManagementPolicy() != nil && !ContainsOnlyManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionAll)) {
log.Debug(errManagementPolicy, "policy", managed.GetManagementPolicy())
record.Event(managed, event.Warning(reasonManagementPolicyNotEnabled, errors.New(errManagementPolicy)))
managed.SetConditions(xpv1.ReconcileError(errors.New(errManagementPolicy)))
Expand Down Expand Up @@ -816,41 +817,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 r.managementPoliciesEnabled && !observation.ResourceExists && ContainsOnlyManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionObserve) {
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
Expand All @@ -865,9 +839,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 && !shouldOrphan(r.managementPoliciesEnabled, managed) {
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
Expand Down Expand Up @@ -940,7 +912,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 && (!r.managementPoliciesEnabled || ContainsManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionCreate)) {
// 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
Expand Down Expand Up @@ -1030,7 +1002,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 && (!r.managementPoliciesEnabled || ContainsManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionLateInitialize)) {
// 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
Expand Down Expand Up @@ -1062,6 +1034,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 r.managementPoliciesEnabled && !ContainsManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionUpdate) {
log.Debug("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,
Expand Down Expand Up @@ -1096,28 +1075,55 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}

// ContainsManagementAction returns true if the management policy contains the management action or ManagementActionAll.
func ContainsManagementAction(managementPolicy xpv1.ManagementPolicy, action xpv1.ManagementAction) bool {
for _, a := range managementPolicy {
if a == action || a == xpv1.ManagementActionAll {
return true
}
}
return false
}

// ContainsOnlyManagementAction returns true if the management policy contains only the management action.
func ContainsOnlyManagementAction(managementPolicy xpv1.ManagementPolicy, action xpv1.ManagementAction) bool {
return len(managementPolicy) == 1 && managementPolicy[0] == action
}

// 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
// orphaning of the external resource. This function implements 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".
// is set to "Delete" and the managementPolicy includes ManagementPolicyDeleteAction.
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.

// delete external resource if both the deletionPolicy and the managementPolicy are set to delete
if managed.GetDeletionPolicy() == xpv1.DeletionDelete && ContainsManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionDelete) {
return false
}
// if the managementPolicy is not default, and it contains the deletion action, we should delete the external resource
if !ContainsOnlyManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionAll) &&
ContainsManagementAction(managed.GetManagementPolicy(), xpv1.ManagementActionDelete) {
return false
}

// For all other cases, we should orphan the external resource.
// Obvious cases:
// DeletionOrphan && ManagementOrphanOnDelete
// DeletionOrphan && ManagementObserveOnly
// DeletionOrphan && ManagementPolicy without Delete Action
// Conflicting cases:
// DeletionOrphan && ManagementFullControl (obeys non-default configuration)
// DeletionDelete && ManagementObserveOnly (obeys non-default configuration)
// DeletionDelete && ManagementOrphanOnDelete (obeys non-default configuration)
// DeletionOrphan && Management Policy ["*"] (obeys non-default configuration)
// DeletionDelete && ManagementPolicy that does not include the Delete Action (obeys non-default configuration)
return true
}

// IsPaused returns true if the object has the AnnotationKeyReconciliationPaused
// annotation set to `true` or if the managed policies alpha feature is enabled and
// the `spec.managementPolicy` array is empty.
func IsPaused(o resource.Managed, managementPoliciesEnabled bool) bool {
return o.GetAnnotations()[meta.AnnotationKeyReconciliationPaused] == "true" ||
(managementPoliciesEnabled && len(o.GetManagementPolicy()) == 0)
}
Loading

0 comments on commit ab78f63

Please sign in to comment.