1369 lines
62 KiB
Go
1369 lines
62 KiB
Go
/*
|
|
Copyright 2019 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 (
|
|
"context"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"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"
|
|
|
|
"github.com/crossplane/crossplane-runtime/apis/changelogs/proto/v1alpha1"
|
|
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
|
"github.com/crossplane/crossplane-runtime/pkg/conditions"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
// FinalizerName is the string that is used as finalizer on managed resource
|
|
// objects.
|
|
FinalizerName = "finalizer.managedresource.crossplane.io"
|
|
|
|
reconcileGracePeriod = 30 * time.Second
|
|
reconcileTimeout = 1 * time.Minute
|
|
|
|
defaultPollInterval = 1 * time.Minute
|
|
defaultGracePeriod = 30 * time.Second
|
|
)
|
|
|
|
// 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"
|
|
errReconcileConnect = "connect failed"
|
|
errReconcileObserve = "observe failed"
|
|
errReconcileCreate = "create failed"
|
|
errReconcileUpdate = "update failed"
|
|
errReconcileDelete = "delete failed"
|
|
errRecordChangeLog = "cannot record change log entry"
|
|
|
|
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"
|
|
reasonManagementPolicyInvalid event.Reason = "CannotUseInvalidManagementPolicy"
|
|
|
|
reasonDeleted event.Reason = "DeletedExternalResource"
|
|
reasonCreated event.Reason = "CreatedExternalResource"
|
|
reasonUpdated event.Reason = "UpdatedExternalResource"
|
|
reasonPending event.Reason = "PendingExternalResource"
|
|
|
|
reasonReconciliationPaused event.Reason = "ReconciliationPaused"
|
|
)
|
|
|
|
// ControllerName returns the recommended name for controllers that use this
|
|
// package to reconcile a particular kind of managed resource.
|
|
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 { //nolint:interfacebloat // This has to be big.
|
|
// 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 {
|
|
UpdateCriticalAnnotations(ctx context.Context, o client.Object) error
|
|
}
|
|
|
|
// A CriticalAnnotationUpdateFn may be used when it is critical that annotations
|
|
// must be updated before returning from the Reconcile loop.
|
|
type CriticalAnnotationUpdateFn func(ctx context.Context, o client.Object) error
|
|
|
|
// UpdateCriticalAnnotations of the supplied object.
|
|
func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error {
|
|
return fn(ctx, o)
|
|
}
|
|
|
|
// ConnectionDetails created or updated during an operation on an external
|
|
// resource, for example usernames, passwords, endpoints, ports, etc.
|
|
type ConnectionDetails map[string][]byte
|
|
|
|
// AdditionalDetails represent any additional details the external client wants
|
|
// to return about an operation that has been performed. These details will be
|
|
// included in the change logs.
|
|
type AdditionalDetails map[string]string
|
|
|
|
// A ConnectionPublisher manages the supplied ConnectionDetails for the
|
|
// supplied Managed resource. ManagedPublishers must handle the case in which
|
|
// the supplied ConnectionDetails are empty.
|
|
type ConnectionPublisher interface {
|
|
// PublishConnection details for the supplied Managed resource. Publishing
|
|
// must be additive; i.e. if details (a, b, c) are published, subsequently
|
|
// publicing details (b, c, d) should update (b, c) but not remove a.
|
|
PublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) (published bool, err error)
|
|
|
|
// UnpublishConnection details for the supplied Managed resource.
|
|
UnpublishConnection(ctx context.Context, so resource.ConnectionSecretOwner, c ConnectionDetails) error
|
|
}
|
|
|
|
// ConnectionPublisherFns is the pluggable struct to produce objects with ConnectionPublisher interface.
|
|
type ConnectionPublisherFns struct {
|
|
PublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error)
|
|
UnpublishConnectionFn func(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error
|
|
}
|
|
|
|
// PublishConnection details for the supplied Managed resource.
|
|
func (fn ConnectionPublisherFns) PublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) (bool, error) {
|
|
return fn.PublishConnectionFn(ctx, o, c)
|
|
}
|
|
|
|
// UnpublishConnection details for the supplied Managed resource.
|
|
func (fn ConnectionPublisherFns) UnpublishConnection(ctx context.Context, o resource.ConnectionSecretOwner, c ConnectionDetails) error {
|
|
return fn.UnpublishConnectionFn(ctx, o, c)
|
|
}
|
|
|
|
// A ConnectionDetailsFetcher fetches connection details for the supplied
|
|
// Connection Secret owner.
|
|
type ConnectionDetailsFetcher interface {
|
|
FetchConnection(ctx context.Context, so resource.ConnectionSecretOwner) (ConnectionDetails, error)
|
|
}
|
|
|
|
// Initializer establishes ownership of the supplied Managed resource.
|
|
// This typically involves the operations that are run before calling any
|
|
// ExternalClient methods.
|
|
type Initializer interface {
|
|
Initialize(ctx context.Context, mg resource.Managed) error
|
|
}
|
|
|
|
// A InitializerChain chains multiple managed initializers.
|
|
type InitializerChain []Initializer
|
|
|
|
// Initialize calls each Initializer serially. It returns the first
|
|
// error it encounters, if any.
|
|
func (cc InitializerChain) Initialize(ctx context.Context, mg resource.Managed) error {
|
|
for _, c := range cc {
|
|
if err := c.Initialize(ctx, mg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A InitializerFn is a function that satisfies the Initializer
|
|
// interface.
|
|
type InitializerFn func(ctx context.Context, mg resource.Managed) error
|
|
|
|
// Initialize calls InitializerFn function.
|
|
func (m InitializerFn) Initialize(ctx context.Context, mg resource.Managed) error {
|
|
return m(ctx, mg)
|
|
}
|
|
|
|
// A ReferenceResolver resolves references to other managed resources.
|
|
type ReferenceResolver interface {
|
|
// ResolveReferences resolves all fields in the supplied managed resource
|
|
// that are references to other managed resources by updating corresponding
|
|
// fields, for example setting spec.network to the Network resource
|
|
// specified by spec.networkRef.name.
|
|
ResolveReferences(ctx context.Context, mg resource.Managed) error
|
|
}
|
|
|
|
// A ReferenceResolverFn is a function that satisfies the
|
|
// ReferenceResolver interface.
|
|
type ReferenceResolverFn func(context.Context, resource.Managed) error
|
|
|
|
// ResolveReferences calls ReferenceResolverFn function.
|
|
func (m ReferenceResolverFn) ResolveReferences(ctx context.Context, mg resource.Managed) error {
|
|
return m(ctx, mg)
|
|
}
|
|
|
|
// An ExternalConnector produces a new ExternalClient given the supplied
|
|
// Managed resource.
|
|
type ExternalConnector = TypedExternalConnector[resource.Managed]
|
|
|
|
// A TypedExternalConnector produces a new ExternalClient given the supplied
|
|
// Managed resource.
|
|
type TypedExternalConnector[managed resource.Managed] interface {
|
|
// Connect to the provider specified by the supplied managed resource and
|
|
// produce an ExternalClient.
|
|
Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
|
|
}
|
|
|
|
// A NopDisconnector converts an ExternalConnector into an
|
|
// ExternalConnectDisconnector with a no-op Disconnect method.
|
|
type NopDisconnector = TypedNopDisconnector[resource.Managed]
|
|
|
|
// A TypedNopDisconnector converts an ExternalConnector into an
|
|
// ExternalConnectDisconnector with a no-op Disconnect method.
|
|
type TypedNopDisconnector[managed resource.Managed] struct {
|
|
c TypedExternalConnector[managed]
|
|
}
|
|
|
|
// Connect calls the underlying ExternalConnector's Connect method.
|
|
func (c *TypedNopDisconnector[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
|
|
return c.c.Connect(ctx, mg)
|
|
}
|
|
|
|
// Disconnect does nothing. It never returns an error.
|
|
func (c *TypedNopDisconnector[managed]) Disconnect(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// NewNopDisconnector converts an ExternalConnector into an
|
|
// ExternalConnectDisconnector with a no-op Disconnect method.
|
|
func NewNopDisconnector(c ExternalConnector) ExternalConnectDisconnector {
|
|
return NewTypedNopDisconnector(c)
|
|
}
|
|
|
|
// NewTypedNopDisconnector converts an TypedExternalConnector into an
|
|
// ExternalConnectDisconnector with a no-op Disconnect method.
|
|
func NewTypedNopDisconnector[managed resource.Managed](c TypedExternalConnector[managed]) TypedExternalConnectDisconnector[managed] {
|
|
return &TypedNopDisconnector[managed]{c}
|
|
}
|
|
|
|
// An ExternalConnectDisconnector produces a new ExternalClient given the supplied
|
|
// Managed resource.
|
|
type ExternalConnectDisconnector = TypedExternalConnectDisconnector[resource.Managed]
|
|
|
|
// A TypedExternalConnectDisconnector produces a new ExternalClient given the supplied
|
|
// Managed resource.
|
|
type TypedExternalConnectDisconnector[managed resource.Managed] interface {
|
|
TypedExternalConnector[managed]
|
|
ExternalDisconnector
|
|
}
|
|
|
|
// An ExternalConnectorFn is a function that satisfies the ExternalConnector
|
|
// interface.
|
|
type ExternalConnectorFn = TypedExternalConnectorFn[resource.Managed]
|
|
|
|
// An TypedExternalConnectorFn is a function that satisfies the
|
|
// TypedExternalConnector interface.
|
|
type TypedExternalConnectorFn[managed resource.Managed] func(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
|
|
|
|
// Connect to the provider specified by the supplied managed resource and
|
|
// produce an ExternalClient.
|
|
func (ec TypedExternalConnectorFn[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
|
|
return ec(ctx, mg)
|
|
}
|
|
|
|
// An ExternalDisconnectorFn is a function that satisfies the ExternalConnector
|
|
// interface.
|
|
type ExternalDisconnectorFn func(ctx context.Context) error
|
|
|
|
// Disconnect from provider and close the ExternalClient.
|
|
func (ed ExternalDisconnectorFn) Disconnect(ctx context.Context) error {
|
|
return ed(ctx)
|
|
}
|
|
|
|
// ExternalConnectDisconnectorFns are functions that satisfy the
|
|
// ExternalConnectDisconnector interface.
|
|
type ExternalConnectDisconnectorFns = TypedExternalConnectDisconnectorFns[resource.Managed]
|
|
|
|
// TypedExternalConnectDisconnectorFns are functions that satisfy the
|
|
// TypedExternalConnectDisconnector interface.
|
|
type TypedExternalConnectDisconnectorFns[managed resource.Managed] struct {
|
|
ConnectFn func(ctx context.Context, mg managed) (TypedExternalClient[managed], error)
|
|
DisconnectFn func(ctx context.Context) error
|
|
}
|
|
|
|
// Connect to the provider specified by the supplied managed resource and
|
|
// produce an ExternalClient.
|
|
func (fns TypedExternalConnectDisconnectorFns[managed]) Connect(ctx context.Context, mg managed) (TypedExternalClient[managed], error) {
|
|
return fns.ConnectFn(ctx, mg)
|
|
}
|
|
|
|
// Disconnect from the provider and close the ExternalClient.
|
|
func (fns TypedExternalConnectDisconnectorFns[managed]) Disconnect(ctx context.Context) error {
|
|
return fns.DisconnectFn(ctx)
|
|
}
|
|
|
|
// An ExternalClient manages the lifecycle of an external resource.
|
|
// None of the calls here should be blocking. All of the calls should be
|
|
// idempotent. For example, Create call should not return AlreadyExists error
|
|
// if it's called again with the same parameters or Delete call should not
|
|
// return error if there is an ongoing deletion or resource does not exist.
|
|
type ExternalClient = TypedExternalClient[resource.Managed]
|
|
|
|
// A TypedExternalClient manages the lifecycle of an external resource.
|
|
// None of the calls here should be blocking. All of the calls should be
|
|
// idempotent. For example, Create call should not return AlreadyExists error
|
|
// if it's called again with the same parameters or Delete call should not
|
|
// return error if there is an ongoing deletion or resource does not exist.
|
|
type TypedExternalClient[managedType resource.Managed] interface {
|
|
// Observe the external resource the supplied Managed resource
|
|
// represents, if any. Observe implementations must not modify the
|
|
// external resource, but may update the supplied Managed resource to
|
|
// reflect the state of the external resource. Status modifications are
|
|
// automatically persisted unless ResourceLateInitialized is true - see
|
|
// ResourceLateInitialized for more detail.
|
|
Observe(ctx context.Context, mg managedType) (ExternalObservation, error)
|
|
|
|
// Create an external resource per the specifications of the supplied
|
|
// Managed resource. Called when Observe reports that the associated
|
|
// external resource does not exist. Create implementations may update
|
|
// managed resource annotations, and those updates will be persisted.
|
|
// All other updates will be discarded.
|
|
Create(ctx context.Context, mg managedType) (ExternalCreation, error)
|
|
|
|
// Update the external resource represented by the supplied Managed
|
|
// resource, if necessary. Called unless Observe reports that the
|
|
// associated external resource is up to date.
|
|
Update(ctx context.Context, mg managedType) (ExternalUpdate, error)
|
|
|
|
// Delete the external resource upon deletion of its associated Managed
|
|
// resource. Called when the managed resource has been deleted.
|
|
Delete(ctx context.Context, mg managedType) (ExternalDelete, error)
|
|
|
|
// Disconnect from the provider and close the ExternalClient.
|
|
// Called at the end of reconcile loop. An ExternalClient not requiring
|
|
// to explicitly disconnect to cleanup it resources, can provide a no-op
|
|
// implementation which just return nil.
|
|
Disconnect(ctx context.Context) error
|
|
}
|
|
|
|
// ExternalClientFns are a series of functions that satisfy the ExternalClient
|
|
// interface.
|
|
type ExternalClientFns = TypedExternalClientFns[resource.Managed]
|
|
|
|
// TypedExternalClientFns are a series of functions that satisfy the
|
|
// ExternalClient interface.
|
|
type TypedExternalClientFns[managed resource.Managed] struct {
|
|
ObserveFn func(ctx context.Context, mg managed) (ExternalObservation, error)
|
|
CreateFn func(ctx context.Context, mg managed) (ExternalCreation, error)
|
|
UpdateFn func(ctx context.Context, mg managed) (ExternalUpdate, error)
|
|
DeleteFn func(ctx context.Context, mg managed) (ExternalDelete, error)
|
|
DisconnectFn func(ctx context.Context) error
|
|
}
|
|
|
|
// Observe the external resource the supplied Managed resource represents, if
|
|
// any.
|
|
func (e TypedExternalClientFns[managed]) Observe(ctx context.Context, mg managed) (ExternalObservation, error) {
|
|
return e.ObserveFn(ctx, mg)
|
|
}
|
|
|
|
// Create an external resource per the specifications of the supplied Managed
|
|
// resource.
|
|
func (e TypedExternalClientFns[managed]) Create(ctx context.Context, mg managed) (ExternalCreation, error) {
|
|
return e.CreateFn(ctx, mg)
|
|
}
|
|
|
|
// Update the external resource represented by the supplied Managed resource, if
|
|
// necessary.
|
|
func (e TypedExternalClientFns[managed]) Update(ctx context.Context, mg managed) (ExternalUpdate, error) {
|
|
return e.UpdateFn(ctx, mg)
|
|
}
|
|
|
|
// Delete the external resource upon deletion of its associated Managed
|
|
// resource.
|
|
func (e TypedExternalClientFns[managed]) Delete(ctx context.Context, mg managed) (ExternalDelete, error) {
|
|
return e.DeleteFn(ctx, mg)
|
|
}
|
|
|
|
// Disconnect the external client.
|
|
func (e TypedExternalClientFns[managed]) Disconnect(ctx context.Context) error {
|
|
return e.DisconnectFn(ctx)
|
|
}
|
|
|
|
// A NopConnector does nothing.
|
|
type NopConnector struct{}
|
|
|
|
// Connect returns a NopClient. It never returns an error.
|
|
func (c *NopConnector) Connect(_ context.Context, _ resource.Managed) (ExternalClient, error) {
|
|
return &NopClient{}, nil
|
|
}
|
|
|
|
// A NopClient does nothing.
|
|
type NopClient struct{}
|
|
|
|
// Observe does nothing. It returns an empty ExternalObservation and no error.
|
|
func (c *NopClient) Observe(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
|
return ExternalObservation{}, nil
|
|
}
|
|
|
|
// Create does nothing. It returns an empty ExternalCreation and no error.
|
|
func (c *NopClient) Create(_ context.Context, _ resource.Managed) (ExternalCreation, error) {
|
|
return ExternalCreation{}, nil
|
|
}
|
|
|
|
// Update does nothing. It returns an empty ExternalUpdate and no error.
|
|
func (c *NopClient) Update(_ context.Context, _ resource.Managed) (ExternalUpdate, error) {
|
|
return ExternalUpdate{}, nil
|
|
}
|
|
|
|
// Delete does nothing. It never returns an error.
|
|
func (c *NopClient) Delete(_ context.Context, _ resource.Managed) (ExternalDelete, error) {
|
|
return ExternalDelete{}, nil
|
|
}
|
|
|
|
// Disconnect does nothing. It never returns an error.
|
|
func (c *NopClient) Disconnect(_ context.Context) error { return nil }
|
|
|
|
// An ExternalObservation is the result of an observation of an external
|
|
// resource.
|
|
type ExternalObservation struct {
|
|
// ResourceExists must be true if a corresponding external resource exists
|
|
// for the managed resource. Typically this is proven by the presence of an
|
|
// external resource of the expected kind whose unique identifier matches
|
|
// the managed resource's external name. Crossplane uses this information to
|
|
// determine whether it needs to create or delete the external resource.
|
|
ResourceExists bool
|
|
|
|
// ResourceUpToDate should be true if the corresponding external resource
|
|
// appears to be up-to-date - i.e. updating the external resource to match
|
|
// the desired state of the managed resource would be a no-op. Keep in mind
|
|
// that often only a subset of external resource fields can be updated.
|
|
// Crossplane uses this information to determine whether it needs to update
|
|
// the external resource.
|
|
ResourceUpToDate bool
|
|
|
|
// ResourceLateInitialized should be true if the managed resource's spec was
|
|
// updated during its observation. A Crossplane provider may update a
|
|
// managed resource's spec fields after it is created or updated, as long as
|
|
// the updates are limited to setting previously unset fields, and adding
|
|
// keys to maps. Crossplane uses this information to determine whether
|
|
// changes to the spec were made during observation that must be persisted.
|
|
// Note that changes to the spec will be persisted before changes to the
|
|
// status, and that pending changes to the status may be lost when the spec
|
|
// is persisted. Status changes will be persisted by the first subsequent
|
|
// observation that _does not_ late initialize the managed resource, so it
|
|
// is important that Observe implementations do not late initialize the
|
|
// resource every time they are called.
|
|
ResourceLateInitialized bool
|
|
|
|
// ConnectionDetails required to connect to this resource. These details
|
|
// are a set that is collated throughout the managed resource's lifecycle -
|
|
// i.e. returning new connection details will have no affect on old details
|
|
// unless an existing key is overwritten. Crossplane may publish these
|
|
// credentials to a store (e.g. a Secret).
|
|
ConnectionDetails ConnectionDetails
|
|
|
|
// Diff is a Debug level message that is sent to the reconciler when
|
|
// there is a change in the observed Managed Resource. It is useful for
|
|
// finding where the observed diverges from the desired state.
|
|
// The string should be a cmp.Diff that details the difference.
|
|
Diff string
|
|
}
|
|
|
|
// An ExternalCreation is the result of the creation of an external resource.
|
|
type ExternalCreation struct {
|
|
// ConnectionDetails required to connect to this resource. These details
|
|
// are a set that is collated throughout the managed resource's lifecycle -
|
|
// i.e. returning new connection details will have no affect on old details
|
|
// unless an existing key is overwritten. Crossplane may publish these
|
|
// credentials to a store (e.g. a Secret).
|
|
ConnectionDetails ConnectionDetails
|
|
|
|
// AdditionalDetails represent any additional details the external client
|
|
// wants to return about the creation operation that was performed.
|
|
AdditionalDetails AdditionalDetails
|
|
}
|
|
|
|
// An ExternalUpdate is the result of an update to an external resource.
|
|
type ExternalUpdate struct {
|
|
// ConnectionDetails required to connect to this resource. These details
|
|
// are a set that is collated throughout the managed resource's lifecycle -
|
|
// i.e. returning new connection details will have no affect on old details
|
|
// unless an existing key is overwritten. Crossplane may publish these
|
|
// credentials to a store (e.g. a Secret).
|
|
ConnectionDetails ConnectionDetails
|
|
|
|
// AdditionalDetails represent any additional details the external client
|
|
// wants to return about the update operation that was performed.
|
|
AdditionalDetails AdditionalDetails
|
|
}
|
|
|
|
// An ExternalDelete is the result of a deletion of an external resource.
|
|
type ExternalDelete struct {
|
|
// AdditionalDetails represent any additional details the external client
|
|
// wants to return about the delete operation that was performed.
|
|
AdditionalDetails AdditionalDetails
|
|
}
|
|
|
|
// A Reconciler reconciles managed resources by creating and managing the
|
|
// lifecycle of an external resource, i.e. a resource in an external system such
|
|
// as a cloud provider API. Each controller must watch the managed resource kind
|
|
// for which it is responsible.
|
|
type Reconciler struct {
|
|
client client.Client
|
|
newManaged func() resource.Managed
|
|
|
|
pollInterval time.Duration
|
|
pollIntervalHook PollIntervalHook
|
|
|
|
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
|
|
// that the reconciler logic reads r.external.Connect(),
|
|
// r.managed.Delete(), etc.
|
|
external mrExternal
|
|
managed mrManaged
|
|
|
|
conditions conditions.Manager
|
|
|
|
supportedManagementPolicies []sets.Set[xpv1.ManagementAction]
|
|
|
|
log logging.Logger
|
|
record event.Recorder
|
|
metricRecorder MetricRecorder
|
|
change ChangeLogger
|
|
deterministicExternalName bool
|
|
}
|
|
|
|
type mrManaged struct {
|
|
CriticalAnnotationUpdater
|
|
ConnectionPublisher
|
|
resource.Finalizer
|
|
Initializer
|
|
ReferenceResolver
|
|
}
|
|
|
|
func defaultMRManaged(m manager.Manager) mrManaged {
|
|
return mrManaged{
|
|
CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()),
|
|
Finalizer: resource.NewAPIFinalizer(m.GetClient(), FinalizerName),
|
|
Initializer: NewNameAsExternalName(m.GetClient()),
|
|
ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()),
|
|
ConnectionPublisher: PublisherChain([]ConnectionPublisher{
|
|
NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
|
|
&DisabledSecretStoreManager{},
|
|
}),
|
|
}
|
|
}
|
|
|
|
type mrExternal struct {
|
|
ExternalConnectDisconnector
|
|
}
|
|
|
|
func defaultMRExternal() mrExternal {
|
|
return mrExternal{
|
|
ExternalConnectDisconnector: NewNopDisconnector(&NopConnector{}),
|
|
}
|
|
}
|
|
|
|
// A ReconcilerOption configures a Reconciler.
|
|
type ReconcilerOption func(*Reconciler)
|
|
|
|
// WithTimeout specifies the timeout duration cumulatively for all the calls happen
|
|
// in the reconciliation function. In case the deadline exceeds, reconciler will
|
|
// still have some time to make the necessary calls to report the error such as
|
|
// status update.
|
|
func WithTimeout(duration time.Duration) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.timeout = duration
|
|
}
|
|
}
|
|
|
|
// WithPollInterval specifies how long the Reconciler should wait before queueing
|
|
// a new reconciliation after a successful reconcile. The Reconciler requeues
|
|
// after a specified duration when it is not actively waiting for an external
|
|
// operation, but wishes to check whether an existing external resource needs to
|
|
// be synced to its Crossplane Managed resource.
|
|
func WithPollInterval(after time.Duration) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.pollInterval = after
|
|
}
|
|
}
|
|
|
|
// WithMetricRecorder configures the Reconciler to use the supplied MetricRecorder.
|
|
func WithMetricRecorder(recorder MetricRecorder) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.metricRecorder = recorder
|
|
}
|
|
}
|
|
|
|
// PollIntervalHook represents the function type passed to the
|
|
// WithPollIntervalHook option to support dynamic computation of the poll
|
|
// interval.
|
|
type PollIntervalHook func(managed resource.Managed, pollInterval time.Duration) time.Duration
|
|
|
|
func defaultPollIntervalHook(managed resource.Managed, pollInterval time.Duration) time.Duration {
|
|
if managed != nil &&
|
|
managed.GetCondition(xpv1.TypeSynced).Status == v1.ConditionTrue &&
|
|
managed.GetCondition(xpv1.TypeReady).Status == v1.ConditionTrue {
|
|
jitter := 30 * time.Minute
|
|
return time.Hour + time.Duration((rand.Float64()-0.5)*2*float64(jitter)).Round(time.Second)
|
|
}
|
|
return pollInterval
|
|
}
|
|
|
|
// WithPollIntervalHook adds a hook that can be used to configure the
|
|
// delay before an up-to-date resource is reconciled again after a successful
|
|
// reconcile. If this option is passed multiple times, only the latest hook
|
|
// will be used.
|
|
func WithPollIntervalHook(hook PollIntervalHook) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.pollIntervalHook = hook
|
|
}
|
|
}
|
|
|
|
// WithPollJitterHook adds a simple PollIntervalHook to add jitter to the poll
|
|
// interval used when queuing a new reconciliation after a successful
|
|
// reconcile. The added jitter will be a random duration between -jitter and
|
|
// +jitter. This option wraps WithPollIntervalHook, and is subject to the same
|
|
// constraint that only the latest hook will be used.
|
|
func WithPollJitterHook(jitter time.Duration) ReconcilerOption {
|
|
return WithPollIntervalHook(func(_ resource.Managed, pollInterval time.Duration) time.Duration {
|
|
return pollInterval + time.Duration((rand.Float64()-0.5)*2*float64(jitter)) //nolint:gosec // No need for secure randomness.
|
|
})
|
|
}
|
|
|
|
// WithCreationGracePeriod configures an optional period during which we will
|
|
// wait for the external API to report that a newly created external resource
|
|
// exists. This allows us to tolerate eventually consistent APIs that do not
|
|
// immediately report that newly created resources exist when queried. All
|
|
// resources have a 30 second grace period by default.
|
|
func WithCreationGracePeriod(d time.Duration) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.creationGracePeriod = d
|
|
}
|
|
}
|
|
|
|
// WithExternalConnector specifies how the Reconciler should connect to the API
|
|
// used to sync and delete external resources.
|
|
func WithExternalConnector(c ExternalConnector) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.external.ExternalConnectDisconnector = NewNopDisconnector(c)
|
|
}
|
|
}
|
|
|
|
// WithTypedExternalConnector specifies how the Reconciler should connect to the API
|
|
// used to sync and delete external resources.
|
|
func WithTypedExternalConnector[managed resource.Managed](c TypedExternalConnector[managed]) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.external.ExternalConnectDisconnector = &typedExternalConnectDisconnectorWrapper[managed]{
|
|
c: NewTypedNopDisconnector(c),
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithCriticalAnnotationUpdater specifies how the Reconciler should update a
|
|
// managed resource's critical annotations. Implementations typically contain
|
|
// some kind of retry logic to increase the likelihood that critical annotations
|
|
// (like non-deterministic external names) will be persisted.
|
|
func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.managed.CriticalAnnotationUpdater = u
|
|
}
|
|
}
|
|
|
|
// WithConnectionPublishers specifies how the Reconciler should publish
|
|
// its connection details such as credentials and endpoints.
|
|
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.managed.ConnectionPublisher = PublisherChain(p)
|
|
}
|
|
}
|
|
|
|
// WithInitializers specifies how the Reconciler should initialize a
|
|
// managed resource before calling any of the ExternalClient functions.
|
|
func WithInitializers(i ...Initializer) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.managed.Initializer = InitializerChain(i)
|
|
}
|
|
}
|
|
|
|
// WithFinalizer specifies how the Reconciler should add and remove
|
|
// finalizers to and from the managed resource.
|
|
func WithFinalizer(f resource.Finalizer) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.managed.Finalizer = f
|
|
}
|
|
}
|
|
|
|
// WithReferenceResolver specifies how the Reconciler should resolve any
|
|
// inter-resource references it encounters while reconciling managed resources.
|
|
func WithReferenceResolver(rr ReferenceResolver) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.managed.ReferenceResolver = rr
|
|
}
|
|
}
|
|
|
|
// WithLogger specifies how the Reconciler should log messages.
|
|
func WithLogger(l logging.Logger) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.log = l
|
|
}
|
|
}
|
|
|
|
// WithRecorder specifies how the Reconciler should record events.
|
|
func WithRecorder(er event.Recorder) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.record = er
|
|
}
|
|
}
|
|
|
|
// WithManagementPolicies enables support for management policies.
|
|
func WithManagementPolicies() ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.features.Enable(feature.EnableBetaManagementPolicies)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// WithChangeLogger enables support for capturing change logs during
|
|
// reconciliation.
|
|
func WithChangeLogger(c ChangeLogger) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.change = c
|
|
}
|
|
}
|
|
|
|
// WithDeterministicExternalName specifies that the external name of the MR is
|
|
// deterministic. If this value is not "true", the provider will not re-queue the
|
|
// managed resource in scenarios where creation is deemed incomplete. This behaviour
|
|
// is a safeguard to avoid a leaked resource due to a non-deterministic name generated
|
|
// by the external system. Conversely, if this value is "true", signifying that the
|
|
// managed resources is deterministically named by the external system, then this
|
|
// safeguard is ignored as it is safe to re-queue a deterministically named resource.
|
|
func WithDeterministicExternalName(b bool) ReconcilerOption {
|
|
return func(r *Reconciler) {
|
|
r.deterministicExternalName = b
|
|
}
|
|
}
|
|
|
|
// NewReconciler returns a Reconciler that reconciles managed resources of the
|
|
// supplied ManagedKind with resources in an external system such as a cloud
|
|
// provider API. It panics if asked to reconcile a managed resource kind that is
|
|
// not registered with the supplied manager's runtime.Scheme. The returned
|
|
// Reconciler reconciles with a dummy, no-op 'external system' by default;
|
|
// callers should supply an ExternalConnector that returns an ExternalClient
|
|
// capable of managing resources in a real system.
|
|
func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOption) *Reconciler {
|
|
nm := func() resource.Managed {
|
|
//nolint:forcetypeassert // If this isn't an MR it's a programming error and we want to panic.
|
|
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Managed)
|
|
}
|
|
|
|
// Panic early if we've been asked to reconcile a resource kind that has not
|
|
// been registered with our controller manager's scheme.
|
|
_ = nm()
|
|
|
|
r := &Reconciler{
|
|
client: m.GetClient(),
|
|
newManaged: nm,
|
|
pollInterval: defaultPollInterval,
|
|
pollIntervalHook: defaultPollIntervalHook,
|
|
creationGracePeriod: defaultGracePeriod,
|
|
timeout: reconcileTimeout,
|
|
managed: defaultMRManaged(m),
|
|
external: defaultMRExternal(),
|
|
supportedManagementPolicies: defaultSupportedManagementPolicies(),
|
|
log: logging.NewNopLogger(),
|
|
record: event.NewNopRecorder(),
|
|
metricRecorder: NewNopMetricRecorder(),
|
|
change: newNopChangeLogger(),
|
|
conditions: new(conditions.ObservedGenerationPropagationManager),
|
|
}
|
|
|
|
for _, ro := range o {
|
|
ro(r)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// Reconcile a managed resource with an external resource.
|
|
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (result reconcile.Result, err error) { //nolint:gocognit // See note below.
|
|
// NOTE(negz): This method is a well over our cyclomatic complexity goal.
|
|
// Be wary of adding additional complexity.
|
|
|
|
defer func() { result, err = errors.SilentlyRequeueOnConflict(result, err) }()
|
|
|
|
log := r.log.WithValues("request", req)
|
|
log.Debug("Reconciling")
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, r.timeout+reconcileGracePeriod)
|
|
defer cancel()
|
|
|
|
externalCtx, externalCancel := context.WithTimeout(ctx, r.timeout)
|
|
defer externalCancel()
|
|
|
|
managed := r.newManaged()
|
|
if err := r.client.Get(ctx, req.NamespacedName, managed); err != nil {
|
|
// There's no need to requeue if we no longer exist. Otherwise we'll be
|
|
// requeued implicitly because we return an error.
|
|
log.Debug("Cannot get managed resource", "error", err)
|
|
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetManaged)
|
|
}
|
|
|
|
r.metricRecorder.recordFirstTimeReconciled(managed)
|
|
status := r.conditions.For(managed)
|
|
|
|
record := r.record.WithAnnotations("external-name", meta.GetExternalName(managed))
|
|
log = log.WithValues(
|
|
"uid", managed.GetUID(),
|
|
"version", managed.GetResourceVersion(),
|
|
"external-name", meta.GetExternalName(managed),
|
|
)
|
|
|
|
managementPoliciesEnabled := r.features.Enabled(feature.EnableBetaManagementPolicies)
|
|
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))
|
|
status.MarkConditions(xpv1.ReconcilePaused())
|
|
// 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 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. 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())
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonManagementPolicyInvalid, err))
|
|
status.MarkConditions(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) && !policy.ShouldDelete() {
|
|
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
|
|
|
|
// Empty ConnectionDetails are passed to UnpublishConnection because we
|
|
// have not retrieved them from the external resource. In practice we
|
|
// currently only write connection details to a Secret, and we rely on
|
|
// garbage collection to delete the entire secret, regardless of the
|
|
// supplied connection details.
|
|
if err := r.managed.UnpublishConnection(ctx, managed, 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 unpublish connection details", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotUnpublish, err))
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
if err := r.managed.RemoveFinalizer(ctx, managed); 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 remove managed resource finalizer", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// We've successfully unpublished our managed resource's connection
|
|
// details and removed our finalizer. If we assume we were the only
|
|
// controller that added a finalizer to this resource then it should no
|
|
// longer exist and thus there is no point trying to update its status.
|
|
r.metricRecorder.recordDeleted(managed)
|
|
log.Debug("Successfully deleted managed resource")
|
|
return reconcile.Result{Requeue: false}, nil
|
|
}
|
|
|
|
if err := r.managed.Initialize(ctx, managed); 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 initialize managed resource", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotInitialize, err))
|
|
status.MarkConditions(xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// If we started but never completed creation of an external resource we
|
|
// may have lost critical information. For example if we didn't persist
|
|
// an updated external name which is non-deterministic, we have leaked a
|
|
// resource. The safest thing to do is to refuse to proceed. However, if
|
|
// the resource has a deterministic external name, it is safe to proceed.
|
|
if meta.ExternalCreateIncomplete(managed) {
|
|
if !r.deterministicExternalName {
|
|
log.Debug(errCreateIncomplete)
|
|
record.Event(managed, event.Warning(reasonCannotInitialize, errors.New(errCreateIncomplete)))
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(errors.New(errCreateIncomplete)))
|
|
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
log.Debug("Cannot determine creation result, but proceeding due to deterministic external name")
|
|
}
|
|
|
|
// We resolve any references before observing our external resource because
|
|
// in some rare examples we need a spec field to make the observe call, and
|
|
// that spec field could be set by a reference.
|
|
//
|
|
// We do not resolve references when being deleted because it is likely that
|
|
// the resources we reference are also being deleted, and would thus block
|
|
// resolution due to being unready or non-existent. It is unlikely (but not
|
|
// impossible) that we need to resolve a reference in order to process a
|
|
// delete, and that reference is stale at delete time.
|
|
if !meta.WasDeleted(managed) {
|
|
if err := r.managed.ResolveReferences(ctx, managed); err != nil {
|
|
// If any of our referenced resources are not yet ready (or if we
|
|
// encountered an error resolving them) we want to try again. If
|
|
// this is the first time we encounter this situation we'll be
|
|
// requeued implicitly due to the status update. If not, we want
|
|
// requeue explicitly, which will trigger backoff.
|
|
log.Debug("Cannot resolve managed resource references", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotResolveRefs, err))
|
|
status.MarkConditions(xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
}
|
|
|
|
external, err := r.external.Connect(externalCtx, managed)
|
|
if err != nil {
|
|
// We'll usually hit this case if our Provider or its secret are missing
|
|
// or invalid. If this is 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 connect to provider", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotConnect, err))
|
|
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileConnect)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
defer func() {
|
|
if err := r.external.Disconnect(ctx); err != nil {
|
|
log.Debug("Cannot disconnect from provider", "error", err)
|
|
record.Event(managed, event.Warning(reasonCannotDisconnect, err))
|
|
}
|
|
|
|
if err := external.Disconnect(ctx); err != nil {
|
|
log.Debug("Cannot disconnect from provider", "error", err)
|
|
record.Event(managed, event.Warning(reasonCannotDisconnect, err))
|
|
}
|
|
}()
|
|
|
|
observation, err := external.Observe(externalCtx, managed)
|
|
if err != nil {
|
|
// We'll usually hit this case if our Provider credentials are invalid
|
|
// or insufficient for observing the external resource type we're
|
|
// concerned with. 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 observe external resource", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotObserve, err))
|
|
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileObserve)))
|
|
return reconcile.Result{Requeue: true}, 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)))
|
|
status.MarkConditions(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
|
|
// consistent and may report that a recently created resource does not
|
|
// exist.
|
|
if !observation.ResourceExists && meta.ExternalCreateSucceededDuring(managed, r.creationGracePeriod) {
|
|
log.Debug("Waiting for external resource existence to be confirmed")
|
|
record.Event(managed, event.Normal(reasonPending, "Waiting for external resource existence to be confirmed"))
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
|
|
// deep copy the managed resource now that we've called Observe() and have
|
|
// not performed any external operations - we can use this as the
|
|
// pre-operation managed resource state in the change logs later
|
|
//nolint:forcetypeassert // managed.DeepCopyObject() will always be a resource.Managed.
|
|
managedPreOp := managed.DeepCopyObject().(resource.Managed)
|
|
|
|
if meta.WasDeleted(managed) {
|
|
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
|
|
|
|
if observation.ResourceExists && policy.ShouldDelete() {
|
|
deletion, err := external.Delete(externalCtx, managed)
|
|
if err != nil {
|
|
// We'll hit this condition if we can't delete our external
|
|
// resource, for example if our provider credentials don't have
|
|
// access to delete it. 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 want requeue
|
|
// explicitly, which will trigger backoff.
|
|
log.Debug("Cannot delete external resource", "error", err)
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, err, deletion.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotDelete, err))
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileError(errors.Wrap(err, errReconcileDelete)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// We've successfully requested deletion of our external resource.
|
|
// We queue another reconcile after a short wait rather than
|
|
// immediately finalizing our delete in order to verify that the
|
|
// external resource was actually deleted. If it no longer exists
|
|
// we'll skip this block on the next reconcile and proceed to
|
|
// unpublish and finalize. If it still exists we'll re-enter this
|
|
// block and try again.
|
|
log.Debug("Successfully requested deletion of external resource")
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_DELETE, nil, deletion.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
record.Event(managed, event.Normal(reasonDeleted, "Successfully requested deletion of external resource"))
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileSuccess())
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
if err := r.managed.UnpublishConnection(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 unpublish connection details", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotUnpublish, err))
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
if err := r.managed.RemoveFinalizer(ctx, managed); 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 remove managed resource finalizer", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
status.MarkConditions(xpv1.Deleting(), xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// We've successfully deleted our external resource (if necessary) and
|
|
// removed our finalizer. If we assume we were the only controller that
|
|
// added a finalizer to this resource then it should no longer exist and
|
|
// thus there is no point trying to update its status.
|
|
r.metricRecorder.recordDeleted(managed)
|
|
log.Debug("Successfully deleted managed resource")
|
|
return reconcile.Result{Requeue: false}, nil
|
|
}
|
|
|
|
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)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotPublish, err))
|
|
status.MarkConditions(xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
if err := r.managed.AddFinalizer(ctx, managed); 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 add finalizer", "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
status.MarkConditions(xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
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
|
|
// subsequent external.Create call. Secondly, it guarantees that
|
|
// we're operating on the latest version of our resource. We
|
|
// don't use the CriticalAnnotationUpdater because we _want_ the
|
|
// update to fail if we get a 409 due to a stale version.
|
|
meta.SetExternalCreatePending(managed, time.Now())
|
|
if err := r.client.Update(ctx, managed); err != nil {
|
|
log.Debug(errUpdateManaged, "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManaged)))
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
creation, err := external.Create(externalCtx, managed)
|
|
if err != nil {
|
|
// We'll hit this condition if we can't create our external
|
|
// resource, for example if our provider credentials don't have
|
|
// access to create it. 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 create external resource", "error", err)
|
|
if !kerrors.IsConflict(err) {
|
|
record.Event(managed, event.Warning(reasonCannotCreate, err))
|
|
}
|
|
|
|
// We handle annotations specially here because it's
|
|
// critical that they are persisted to the API server.
|
|
// If we don't add the external-create-failed annotation
|
|
// the reconciler will refuse to proceed, because it
|
|
// won't know whether or not it created an external
|
|
// resource.
|
|
meta.SetExternalCreateFailed(managed, time.Now())
|
|
if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil {
|
|
log.Debug(errUpdateManagedAnnotations, "error", err)
|
|
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations)))
|
|
|
|
// We only log and emit an event here rather
|
|
// than setting a status condition and returning
|
|
// early because presumably it's more useful to
|
|
// set our status condition to the reason the
|
|
// create failed.
|
|
}
|
|
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, err, creation.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// In some cases our external-name may be set by Create above.
|
|
log = log.WithValues("external-name", meta.GetExternalName(managed))
|
|
record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed))
|
|
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_CREATE, nil, creation.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
|
|
// We handle annotations specially here because it's critical
|
|
// that they are persisted to the API server. If we don't remove
|
|
// add the external-create-succeeded annotation the reconciler
|
|
// will refuse to proceed, because it won't know whether or not
|
|
// it created an external resource. This is also important in
|
|
// cases where we must record an external-name annotation set by
|
|
// the Create call. Any other changes made during Create will be
|
|
// reverted when annotations are updated; at the time of writing
|
|
// Create implementations are advised not to alter status, but
|
|
// we may revisit this in future.
|
|
meta.SetExternalCreateSucceeded(managed, time.Now())
|
|
if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil {
|
|
log.Debug(errUpdateManagedAnnotations, "error", err)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations)))
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAnnotations)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
if _, err := r.managed.PublishConnection(ctx, managed, creation.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)
|
|
if kerrors.IsConflict(err) {
|
|
return reconcile.Result{Requeue: true}, nil
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotPublish, err))
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// We've successfully created our external resource. In many cases the
|
|
// creation process takes a little time to finish. We requeue explicitly
|
|
// order to observe the external resource to determine whether it's
|
|
// ready for use.
|
|
log.Debug("Successfully requested creation of external resource")
|
|
record.Event(managed, event.Normal(reasonCreated, "Successfully requested creation of external resource"))
|
|
status.MarkConditions(xpv1.Creating(), xpv1.ReconcileSuccess())
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
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
|
|
// resource's status, which is subsequently deserialized into managed.
|
|
// This is usually tolerable because the update will implicitly requeue
|
|
// an immediate reconcile which should re-observe the external resource
|
|
// and persist its status.
|
|
if err := r.client.Update(ctx, managed); err != nil {
|
|
log.Debug(errUpdateManaged, "error", err)
|
|
record.Event(managed, event.Warning(reasonCannotUpdateManaged, err))
|
|
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
}
|
|
|
|
if observation.ResourceUpToDate {
|
|
// We did not need to create, update, or delete our external resource.
|
|
// Per the below issue nothing will notify us if and when the external
|
|
// resource we manage changes, so we requeue a speculative reconcile
|
|
// after the specified poll interval in order to observe it and react
|
|
// accordingly.
|
|
// https://github.com/crossplane/crossplane/issues/289
|
|
reconcileAfter := r.pollIntervalHook(managed, r.pollInterval)
|
|
log.Debug("External resource is up to date", "requeue-after", time.Now().Add(reconcileAfter))
|
|
status.MarkConditions(xpv1.ReconcileSuccess())
|
|
r.metricRecorder.recordFirstTimeReady(managed)
|
|
|
|
// record that we intentionally did not update the managed resource
|
|
// because no drift was detected. We call this so late in the reconcile
|
|
// because all the cases above could contribute (for different reasons)
|
|
// that the external object would not have been updated.
|
|
r.metricRecorder.recordUnchanged(managed.GetName())
|
|
|
|
return reconcile.Result{RequeueAfter: reconcileAfter}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
if observation.Diff != "" {
|
|
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() {
|
|
reconcileAfter := r.pollIntervalHook(managed, r.pollInterval)
|
|
log.Debug("Skipping update due to managementPolicies. Reconciliation succeeded", "requeue-after", time.Now().Add(reconcileAfter))
|
|
status.MarkConditions(xpv1.ReconcileSuccess())
|
|
return reconcile.Result{RequeueAfter: reconcileAfter}, 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,
|
|
// for example if our provider credentials don't have access to update
|
|
// it. 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 update external resource")
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, err, update.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
record.Event(managed, event.Warning(reasonCannotUpdate, err))
|
|
status.MarkConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileUpdate)))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// record the drift after the successful update.
|
|
r.metricRecorder.recordDrift(managed)
|
|
if err := r.change.Log(ctx, managedPreOp, v1alpha1.OperationType_OPERATION_TYPE_UPDATE, nil, update.AdditionalDetails); err != nil {
|
|
log.Info(errRecordChangeLog, "error", err)
|
|
}
|
|
|
|
if _, err := r.managed.PublishConnection(ctx, managed, update.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))
|
|
status.MarkConditions(xpv1.ReconcileError(err))
|
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|
|
|
|
// We've successfully updated our external resource. Per the below issue
|
|
// nothing will notify us if and when the external resource we manage
|
|
// changes, so we requeue a speculative reconcile after the specified poll
|
|
// interval in order to observe it and react accordingly.
|
|
// https://github.com/crossplane/crossplane/issues/289
|
|
log.Debug("Successfully requested update of external resource", "requeue-after", time.Now().Add(r.pollInterval))
|
|
record.Event(managed, event.Normal(reasonUpdated, "Successfully requested update of external resource"))
|
|
status.MarkConditions(xpv1.ReconcileSuccess())
|
|
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
|
}
|