crossplane-runtime/pkg/resource/api.go

316 lines
12 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"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
util "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
// Error strings.
const (
errCreateManaged = "cannot create managed resource"
errUpdateClaim = "cannot update resource claim"
errGetSecret = "cannot get managed resource's connection secret"
errSecretConflict = "cannot establish control of existing connection secret"
errUpdateSecret = "cannot update connection secret"
errCreateOrUpdateSecret = "cannot create or update connection secret"
errUpdateManaged = "cannot update managed resource"
errUpdateManagedStatus = "cannot update managed resource status"
)
const claimFinalizerName = "finalizer." + claimControllerName
// An APIManagedCreator creates resources by submitting them to a Kubernetes
// API server.
type APIManagedCreator struct {
client client.Client
typer runtime.ObjectTyper
}
// NewAPIManagedCreator returns a new APIManagedCreator.
func NewAPIManagedCreator(c client.Client, t runtime.ObjectTyper) *APIManagedCreator {
return &APIManagedCreator{client: c, typer: t}
}
// Create the supplied resource using the supplied class and claim.
func (a *APIManagedCreator) Create(ctx context.Context, cm Claim, cs Class, mg Managed) error {
cmr := meta.ReferenceTo(cm, MustGetKind(cm, a.typer))
csr := meta.ReferenceTo(cs, MustGetKind(cs, a.typer))
mg.SetClaimReference(cmr)
mg.SetClassReference(csr)
if err := a.client.Create(ctx, mg); err != nil {
return errors.Wrap(err, errCreateManaged)
}
// Since we use GenerateName feature of ObjectMeta, final name of the
// resource is calculated during the creation of the resource. So, we
// can generate a complete reference only after the creation.
mgr := meta.ReferenceTo(mg, MustGetKind(mg, a.typer))
cm.SetResourceReference(mgr)
return errors.Wrap(a.client.Update(ctx, cm), errUpdateClaim)
}
// An APIManagedConnectionPropagator propagates connection details by reading
// them from and writing them to a Kubernetes API server.
type APIManagedConnectionPropagator struct {
client client.Client
typer runtime.ObjectTyper
}
// NewAPIManagedConnectionPropagator returns a new APIManagedConnectionPropagator.
func NewAPIManagedConnectionPropagator(c client.Client, t runtime.ObjectTyper) *APIManagedConnectionPropagator {
return &APIManagedConnectionPropagator{client: c, typer: t}
}
// PropagateConnection details from the supplied resource to the supplied claim.
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, cm Claim, mg Managed) error {
// Either this resource does not expose a connection secret, or this claim
// does not want one.
if mg.GetWriteConnectionSecretToReference() == nil || cm.GetWriteConnectionSecretToReference() == nil {
return nil
}
n := types.NamespacedName{
Namespace: mg.GetWriteConnectionSecretToReference().Namespace,
Name: mg.GetWriteConnectionSecretToReference().Name,
}
mgcs := &corev1.Secret{}
if err := a.client.Get(ctx, n, mgcs); err != nil {
return errors.Wrap(err, errGetSecret)
}
// Make sure the managed resource is the controller of the connection secret
// it references before we propagate it. This ensures a managed resource
// cannot use Crossplane to circumvent RBAC by propagating a secret it does
// not own.
if c := metav1.GetControllerOf(mgcs); c == nil || c.UID != mg.GetUID() {
return errors.New(errSecretConflict)
}
cmcs := LocalConnectionSecretFor(cm, MustGetKind(cm, a.typer))
if _, err := util.CreateOrUpdate(ctx, a.client, cmcs, func() error {
// Inside this anonymous function cmcs could either be unchanged (if
// it does not exist in the API server) or updated to reflect its
// current state according to the API server.
if c := metav1.GetControllerOf(cmcs); c == nil || c.UID != cm.GetUID() {
return errors.New(errSecretConflict)
}
cmcs.Data = mgcs.Data
meta.AddAnnotations(cmcs, map[string]string{
AnnotationKeyPropagateFromNamespace: mgcs.GetNamespace(),
AnnotationKeyPropagateFromName: mgcs.GetName(),
AnnotationKeyPropagateFromUID: string(mgcs.GetUID()),
})
return nil
}); err != nil {
return errors.Wrap(err, errCreateOrUpdateSecret)
}
meta.AddAnnotations(mgcs, map[string]string{
AnnotationKeyPropagateToNamespace: cmcs.GetNamespace(),
AnnotationKeyPropagateToName: cmcs.GetName(),
AnnotationKeyPropagateToUID: string(cmcs.GetUID()),
})
return errors.Wrap(a.client.Update(ctx, mgcs), errUpdateSecret)
}
// An APIBinder binds resources to claims by updating them in a Kubernetes API
// server. Note that APIBinder does not support objects using the status
// subresource; such objects should use APIStatusBinder.
type APIBinder struct {
client client.Client
typer runtime.ObjectTyper
}
// NewAPIBinder returns a new APIBinder.
func NewAPIBinder(c client.Client, t runtime.ObjectTyper) *APIBinder {
return &APIBinder{client: c, typer: t}
}
// Bind the supplied resource to the supplied claim.
func (a *APIBinder) Bind(ctx context.Context, cm Claim, mg Managed) error {
cm.SetBindingPhase(v1alpha1.BindingPhaseBound)
// This claim reference will already be set for dynamically provisioned
// managed resources, but we need to make sure it's set for statically
// provisioned resources too.
cmr := meta.ReferenceTo(cm, MustGetKind(cm, a.typer))
mg.SetClaimReference(cmr)
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
if err := a.client.Update(ctx, mg); err != nil {
return errors.Wrap(err, errUpdateManaged)
}
if meta.GetExternalName(mg) == "" {
return nil
}
// Propagate back the final name of the external resource to the claim.
meta.SetExternalName(cm, meta.GetExternalName(mg))
return errors.Wrap(a.client.Update(ctx, cm), errUpdateClaim)
}
// Unbind the supplied Claim from the supplied Managed resource.
func (a *APIBinder) Unbind(ctx context.Context, _ Claim, mg Managed) error {
// TODO(negz): We probably want to delete the managed resource here if its
// reclaim policy is delete, rather than relying on garbage collection, per
// https://github.com/crossplaneio/crossplane/issues/550
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
mg.SetClaimReference(nil)
return errors.Wrap(IgnoreNotFound(a.client.Update(ctx, mg)), errUpdateManaged)
}
// An APIStatusBinder binds resources to claims by updating them in a
// Kubernetes API server. Note that APIStatusBinder does not support
// objects that do not use the status subresource; such objects should use
// APIBinder.
type APIStatusBinder struct {
client client.Client
typer runtime.ObjectTyper
}
// NewAPIStatusBinder returns a new APIStatusBinder.
func NewAPIStatusBinder(c client.Client, t runtime.ObjectTyper) *APIStatusBinder {
return &APIStatusBinder{client: c, typer: t}
}
// Bind the supplied resource to the supplied claim.
func (a *APIStatusBinder) Bind(ctx context.Context, cm Claim, mg Managed) error {
cm.SetBindingPhase(v1alpha1.BindingPhaseBound)
// This claim reference will already be set for dynamically provisioned
// managed resources, but we need to make sure it's set for statically
// provisioned resources too.
cmr := meta.ReferenceTo(cm, MustGetKind(cm, a.typer))
mg.SetClaimReference(cmr)
if err := a.client.Update(ctx, mg); err != nil {
return errors.Wrap(err, errUpdateManaged)
}
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
if err := a.client.Status().Update(ctx, mg); err != nil {
return errors.Wrap(err, errUpdateManagedStatus)
}
if meta.GetExternalName(mg) == "" {
return nil
}
// Propagate back the final name of the external resource to the claim.
meta.SetExternalName(cm, meta.GetExternalName(mg))
return errors.Wrap(a.client.Update(ctx, cm), errUpdateClaim)
}
// Unbind the supplied Claim from the supplied Managed resource.
func (a *APIStatusBinder) Unbind(ctx context.Context, _ Claim, mg Managed) error {
// TODO(negz): We probably want to delete the managed resource here if its
// reclaim policy is delete, rather than relying on garbage collection, per
// https://github.com/crossplaneio/crossplane/issues/550
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
mg.SetClaimReference(nil)
if err := a.client.Update(ctx, mg); err != nil {
return errors.Wrap(IgnoreNotFound(err), errUpdateManaged)
}
return errors.Wrap(IgnoreNotFound(a.client.Status().Update(ctx, mg)), errUpdateManagedStatus)
}
// An APIClaimFinalizer adds and removes finalizers to and from a claim.
type APIClaimFinalizer struct {
client client.Client
finalizer string
}
// NewAPIClaimFinalizer returns a new APIClaimFinalizer.
func NewAPIClaimFinalizer(c client.Client, finalizer string) *APIClaimFinalizer {
return &APIClaimFinalizer{client: c, finalizer: finalizer}
}
// AddFinalizer to the supplied Claim.
func (a *APIClaimFinalizer) AddFinalizer(ctx context.Context, cm Claim) error {
if meta.FinalizerExists(cm, a.finalizer) {
return nil
}
meta.AddFinalizer(cm, a.finalizer)
return errors.Wrap(a.client.Update(ctx, cm), errUpdateClaim)
}
// RemoveFinalizer from the supplied Claim.
func (a *APIClaimFinalizer) RemoveFinalizer(ctx context.Context, cm Claim) error {
meta.RemoveFinalizer(cm, a.finalizer)
return errors.Wrap(IgnoreNotFound(a.client.Update(ctx, cm)), errUpdateClaim)
}
// An APIManagedFinalizer adds and removes finalizers to and from a resource.
type APIManagedFinalizer struct {
client client.Client
finalizer string
}
// NewAPIManagedFinalizer returns a new APIManagedFinalizer.
func NewAPIManagedFinalizer(c client.Client, finalizer string) *APIManagedFinalizer {
return &APIManagedFinalizer{client: c, finalizer: finalizer}
}
// AddFinalizer to the supplied Managed resource.
func (a *APIManagedFinalizer) AddFinalizer(ctx context.Context, mg Managed) error {
if meta.FinalizerExists(mg, a.finalizer) {
return nil
}
meta.AddFinalizer(mg, a.finalizer)
return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged)
}
// RemoveFinalizer from the supplied Managed resource.
func (a *APIManagedFinalizer) RemoveFinalizer(ctx context.Context, mg Managed) error {
meta.RemoveFinalizer(mg, a.finalizer)
return errors.Wrap(IgnoreNotFound(a.client.Update(ctx, mg)), errUpdateManaged)
}
// ManagedNameAsExternalName writes the name of the managed resource to
// the external name annotation field in order to be used as name of
// the external resource in provider.
type ManagedNameAsExternalName struct{ client client.Client }
// NewManagedNameAsExternalName returns a new ManagedNameAsExternalName.
func NewManagedNameAsExternalName(c client.Client) *ManagedNameAsExternalName {
return &ManagedNameAsExternalName{client: c}
}
// Initialize the given managed resource.
func (a *ManagedNameAsExternalName) Initialize(ctx context.Context, mg Managed) error {
if meta.GetExternalName(mg) != "" {
return nil
}
meta.SetExternalName(mg, mg.GetName())
return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged)
}