crossplane-runtime/pkg/resource/resource.go

367 lines
13 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 resource
import (
"context"
"strings"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
)
// SecretTypeConnection is the type of Crossplane connection secrets.
const SecretTypeConnection corev1.SecretType = "connection.crossplane.io/v1alpha1"
// External resources are tagged/labelled with the following keys in the cloud
// provider API if the type supports.
const (
ExternalResourceTagKeyKind = "crossplane-kind"
ExternalResourceTagKeyName = "crossplane-name"
ExternalResourceTagKeyProvider = "crossplane-providerconfig"
)
// A ManagedKind contains the type metadata for a kind of managed resource.
type ManagedKind schema.GroupVersionKind
// A CompositeKind contains the type metadata for a kind of composite resource.
type CompositeKind schema.GroupVersionKind
// A CompositeClaimKind contains the type metadata for a kind of composite
// resource claim.
type CompositeClaimKind schema.GroupVersionKind
// ProviderConfigKinds contains the type metadata for a kind of provider config.
type ProviderConfigKinds struct {
Config schema.GroupVersionKind
Usage schema.GroupVersionKind
UsageList schema.GroupVersionKind
}
// A LocalConnectionSecretOwner may create and manage a connection secret in its
// own namespace.
type LocalConnectionSecretOwner interface {
runtime.Object
metav1.Object
LocalConnectionSecretWriterTo
}
// A ConnectionPropagator is responsible for propagating information required to
// connect to a resource.
type ConnectionPropagator interface {
PropagateConnection(ctx context.Context, to LocalConnectionSecretOwner, from ConnectionSecretOwner) error
}
// A ConnectionPropagatorFn is a function that satisfies the
// ConnectionPropagator interface.
type ConnectionPropagatorFn func(ctx context.Context, to LocalConnectionSecretOwner, from ConnectionSecretOwner) error
// A ManagedConnectionPropagator is responsible for propagating information
// required to connect to a managed resource (for example the connection secret)
// from the managed resource to a target.
type ManagedConnectionPropagator interface {
PropagateConnection(ctx context.Context, o LocalConnectionSecretOwner, mg Managed) error
}
// A ManagedConnectionPropagatorFn is a function that satisfies the
// ManagedConnectionPropagator interface.
type ManagedConnectionPropagatorFn func(ctx context.Context, o LocalConnectionSecretOwner, mg Managed) error
// PropagateConnection information from the supplied managed resource to the
// supplied resource claim.
func (fn ManagedConnectionPropagatorFn) PropagateConnection(ctx context.Context, o LocalConnectionSecretOwner, mg Managed) error {
return fn(ctx, o, mg)
}
// LocalConnectionSecretFor creates a connection secret in the namespace of the
// supplied LocalConnectionSecretOwner, assumed to be of the supplied kind.
func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: o.GetNamespace(),
Name: o.GetWriteConnectionSecretToReference().Name,
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))},
},
Type: SecretTypeConnection,
Data: make(map[string][]byte),
}
}
// A ConnectionSecretOwner may create and manage a connection secret in an
// arbitrary namespace.
type ConnectionSecretOwner interface {
runtime.Object
metav1.Object
ConnectionSecretWriterTo
}
// ConnectionSecretFor creates a connection for the supplied
// ConnectionSecretOwner, assumed to be of the supplied kind. The secret is
// written to 'default' namespace if the ConnectionSecretOwner does not specify
// a namespace.
func ConnectionSecretFor(o ConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: o.GetWriteConnectionSecretToReference().Namespace,
Name: o.GetWriteConnectionSecretToReference().Name,
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))},
},
Type: SecretTypeConnection,
Data: make(map[string][]byte),
}
}
// MustCreateObject returns a new Object of the supplied kind. It panics if the
// kind is unknown to the supplied ObjectCreator.
func MustCreateObject(kind schema.GroupVersionKind, oc runtime.ObjectCreater) runtime.Object {
obj, err := oc.New(kind)
if err != nil {
panic(err)
}
return obj
}
// GetKind returns the GroupVersionKind of the supplied object. It return an
// error if the object is unknown to the supplied ObjectTyper, the object is
// unversioned, or the object does not have exactly one registered kind.
func GetKind(obj runtime.Object, ot runtime.ObjectTyper) (schema.GroupVersionKind, error) {
kinds, unversioned, err := ot.ObjectKinds(obj)
if err != nil {
return schema.GroupVersionKind{}, errors.Wrap(err, "cannot get kind of supplied object")
}
if unversioned {
return schema.GroupVersionKind{}, errors.New("supplied object is unversioned")
}
if len(kinds) != 1 {
return schema.GroupVersionKind{}, errors.New("supplied object does not have exactly one kind")
}
return kinds[0], nil
}
// MustGetKind returns the GroupVersionKind of the supplied object. It panics if
// the object is unknown to the supplied ObjectTyper, the object is unversioned,
// or the object does not have exactly one registered kind.
func MustGetKind(obj runtime.Object, ot runtime.ObjectTyper) schema.GroupVersionKind {
gvk, err := GetKind(obj, ot)
if err != nil {
panic(err)
}
return gvk
}
// An ErrorIs function returns true if an error satisfies a particular condition.
type ErrorIs func(err error) bool
// Ignore any errors that satisfy the supplied ErrorIs function by returning
// nil. Errors that do not satisfy the suppled function are returned unmodified.
func Ignore(is ErrorIs, err error) error {
if is(err) {
return nil
}
return err
}
// IgnoreNotFound returns the supplied error, or nil if the error indicates a
// Kubernetes resource was not found.
func IgnoreNotFound(err error) error {
return Ignore(kerrors.IsNotFound, err)
}
// IsConditionTrue returns if condition status is true
func IsConditionTrue(c v1alpha1.Condition) bool {
return c.Status == corev1.ConditionTrue
}
// An Applicator applies changes to an object.
type Applicator interface {
Apply(context.Context, runtime.Object, ...ApplyOption) error
}
// A ClientApplicator may be used to build a single 'client' that satisfies both
// client.Client and Applicator.
type ClientApplicator struct {
client.Client
Applicator
}
// An ApplyFn is a function that satisfies the Applicator interface.
type ApplyFn func(context.Context, runtime.Object, ...ApplyOption) error
// Apply changes to the supplied object.
func (fn ApplyFn) Apply(ctx context.Context, o runtime.Object, ao ...ApplyOption) error {
return fn(ctx, o, ao...)
}
// An ApplyOption is called before patching the current object to match the
// desired object. ApplyOptions are not called if no current object exists.
type ApplyOption func(ctx context.Context, current, desired runtime.Object) error
// UpdateFn returns an ApplyOption that is used to modify the current object to
// match fields of the desired.
func UpdateFn(fn func(current, desired runtime.Object)) ApplyOption {
return func(_ context.Context, c, d runtime.Object) error {
fn(c, d)
return nil
}
}
type errNotControllable struct{ error }
func (e errNotControllable) NotControllable() bool {
return true
}
// IsNotControllable returns true if the supplied error indicates that a
// resource is not controllable - i.e. that it another resource is not and may
// not become its controller reference.
func IsNotControllable(err error) bool {
_, ok := err.(interface {
NotControllable() bool
})
return ok
}
// MustBeControllableBy requires that the current object is controllable by an
// object with the supplied UID. An object is controllable if its controller
// reference matches the supplied UID, or it has no controller reference. An
// error that satisfies IsNotControllable will be returned if the current object
// cannot be controlled by the supplied UID.
func MustBeControllableBy(u types.UID) ApplyOption {
return func(_ context.Context, current, _ runtime.Object) error {
c := metav1.GetControllerOf(current.(metav1.Object))
if c == nil {
return nil
}
if c.UID != u {
return errNotControllable{errors.Errorf("existing object is not controlled by UID %q", u)}
}
return nil
}
}
// ConnectionSecretMustBeControllableBy requires that the current object is a
// connection secret that is controllable by an object with the supplied UID.
// Contemporary connection secrets are of SecretTypeConnection, while legacy
// connection secrets are of corev1.SecretTypeOpaque. Contemporary connection
// secrets are considered controllable if they are already controlled by the
// supplied UID, or have no controller reference. Legacy connection secrets are
// only considered controllable if they are already controlled by the supplied
// UID. It is not safe to assume legacy connection secrets without a controller
// reference are controllable because they are indistinguishable from Kubernetes
// secrets that have nothing to do with Crossplane. An error that satisfies
// IsNotControllable will be returned if the current secret is not a connection
// secret or cannot be controlled by the supplied UID.
func ConnectionSecretMustBeControllableBy(u types.UID) ApplyOption {
return func(_ context.Context, current, _ runtime.Object) error {
s := current.(*corev1.Secret)
c := metav1.GetControllerOf(s)
switch {
case c == nil && s.Type != SecretTypeConnection:
return errNotControllable{errors.Errorf("refusing to modify uncontrolled secret of type %q", s.Type)}
case c == nil:
return nil
case c.UID != u:
return errNotControllable{errors.Errorf("existing secret is not controlled by UID %q", u)}
}
return nil
}
}
// ControllersMustMatch requires the current object to have a controller
// reference, and for that controller reference to match the controller
// reference of the desired object.
//
// Deprecated: Use ControllableBy.
func ControllersMustMatch() ApplyOption {
return func(_ context.Context, current, desired runtime.Object) error {
if !meta.HaveSameController(current.(metav1.Object), desired.(metav1.Object)) {
return errors.New("existing object has a different (or no) controller")
}
return nil
}
}
type errNotAllowed struct{ error }
func (e errNotAllowed) NotAllowed() bool {
return true
}
// IsNotAllowed returns true if the supplied error indicates that an operation
// was not allowed.
func IsNotAllowed(err error) bool {
_, ok := err.(interface {
NotAllowed() bool
})
return ok
}
// AllowUpdateIf will only update the current object if the supplied fn returns
// true. An error that satisfies IsNotAllowed will be returned if the supplied
// function returns false. Creation of a desired object that does not currently
// exist is always allowed.
func AllowUpdateIf(fn func(current, desired runtime.Object) bool) ApplyOption {
return func(_ context.Context, current, desired runtime.Object) error {
if fn(current, desired) {
return nil
}
return errNotAllowed{errors.New("update not allowed")}
}
}
// Apply changes to the supplied object. The object will be created if it does
// not exist, or patched if it does.
//
// Deprecated: use APIPatchingApplicator instead.
func Apply(ctx context.Context, c client.Client, o runtime.Object, ao ...ApplyOption) error {
return NewAPIPatchingApplicator(c).Apply(ctx, o, ao...)
}
// GetExternalTags returns the identifying tags to be used to tag the external
// resource in provider API.
func GetExternalTags(mg Managed) map[string]string {
tags := map[string]string{
ExternalResourceTagKeyKind: strings.ToLower(mg.GetObjectKind().GroupVersionKind().GroupKind().String()),
ExternalResourceTagKeyName: mg.GetName(),
}
switch {
case mg.GetProviderConfigReference() != nil && mg.GetProviderConfigReference().Name != "":
tags[ExternalResourceTagKeyProvider] = mg.GetProviderConfigReference().Name
// TODO(muvaf): Remove the branch once Provider type has been removed from
// everywhere.
case mg.GetProviderReference() != nil && mg.GetProviderReference().Name != "":
tags[ExternalResourceTagKeyProvider] = mg.GetProviderReference().Name
}
return tags
}