Remove support for resource claims and classes

See https://github.com/crossplane/crossplane/issues/1670 for context.

Signed-off-by: Nic Cope <negz@rk0n.org>
This commit is contained in:
Nic Cope 2020-09-20 17:25:32 -07:00
parent e7742464e4
commit 95d71dbd91
31 changed files with 33 additions and 4909 deletions

View File

@ -1,67 +0,0 @@
/*
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 v1alpha1
// BindingPhase represents the current binding phase of a resource or claim.
type BindingPhase string
// Binding phases.
const (
// BindingPhaseUnset resources cannot be bound to another resource because
// they are in an unset binding phase, presumed to be functionally
// equivalent to BindingPhaseUnbindable.
BindingPhaseUnset BindingPhase = ""
// BindingPhaseUnbindable resources cannot be bound to another resource, for
// example because they are currently unavailable, or being created.
BindingPhaseUnbindable BindingPhase = "Unbindable"
// BindingPhaseUnbound resources are available for binding to another
// resource.
BindingPhaseUnbound BindingPhase = "Unbound"
// BindingPhaseBound resources are bound to another resource.
BindingPhaseBound BindingPhase = "Bound"
// BindingPhaseReleased managed resources were bound to a resource claim
// that has since been deleted. Released managed resources cannot be
// reclaimed; they are retained to allow manual clean-up and deletion.
BindingPhaseReleased BindingPhase = "Released"
)
// A BindingStatus represents the bindability and binding status of a resource.
type BindingStatus struct {
// Phase represents the binding phase of a managed resource or claim.
// Unbindable resources cannot be bound, typically because they are
// currently unavailable, or still being created. Unbound resource are
// available for binding, and Bound resources have successfully bound to
// another resource.
// Deprecated. See https://github.com/crossplane/crossplane/issues/1670
// +optional
// +kubebuilder:validation:Enum=Unbindable;Unbound;Bound;Released
Phase BindingPhase `json:"bindingPhase,omitempty"`
}
// SetBindingPhase sets the binding phase of the resource.
func (s *BindingStatus) SetBindingPhase(p BindingPhase) {
s.Phase = p
}
// GetBindingPhase gets the binding phase of the resource.
func (s *BindingStatus) GetBindingPhase() BindingPhase {
return s.Phase
}

View File

@ -16,21 +16,6 @@ limitations under the License.
package v1alpha1
// A ReclaimPolicy determines what should happen to managed resources when their
// bound resource claims are deleted.
type ReclaimPolicy string
const (
// ReclaimDelete means both the managed resource and its underlying external
// resource will be deleted when its bound resource claim is deleted.
ReclaimDelete ReclaimPolicy = "Delete"
// ReclaimRetain means the managed resource will retained when its bound
// resource claim is deleted. Furthermore, its underlying external resource
// will be retained when the managed resource is deleted.
ReclaimRetain ReclaimPolicy = "Retain"
)
// A DeletionPolicy determines what should happen to the underlying external
// resource when a managed resource is deleted.
type DeletionPolicy string

View File

@ -19,20 +19,10 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
// NOTE(negz): Importing this as metav1 appears to break controller-gen's
// deepcopy generation logic. It generates a deepcopy file that omits this
// import and thus does not compile. Importing as v1 fixes this. ¯\_(ツ)_/¯
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
)
// The annotation used to make a resource class the default.
const (
AnnotationDefaultClassKey = "resourceclass.crossplane.io/is-default-class"
AnnotationDefaultClassValue = "true"
)
const (
// ResourceCredentialsSecretEndpointKey is the key inside a connection secret for the connection endpoint
ResourceCredentialsSecretEndpointKey = "endpoint"
@ -130,47 +120,6 @@ func (obj *TypedReference) GroupVersionKind() schema.GroupVersionKind {
// GetObjectKind get the ObjectKind of a TypedReference.
func (obj *TypedReference) GetObjectKind() schema.ObjectKind { return obj }
// A ResourceClaimSpec defines the desired state of a resource claim.
// Deprecated. See https://github.com/crossplane/crossplane/issues/1670
type ResourceClaimSpec struct {
// WriteConnectionSecretToReference specifies the name of a Secret, in the
// same namespace as this resource claim, to which any connection details
// for this resource claim should be written. Connection details frequently
// include the endpoint, username, and password required to connect to the
// managed resource bound to this resource claim.
// +optional
WriteConnectionSecretToReference *LocalSecretReference `json:"writeConnectionSecretToRef,omitempty"`
// TODO(negz): Make the below references immutable once set? Doing so means
// we don't have to track what provisioner was used to create a resource.
// A ClassSelector specifies labels that will be used to select a resource
// class for this claim. If multiple classes match the labels one will be
// chosen at random.
// +optional
ClassSelector *v1.LabelSelector `json:"classSelector,omitempty"`
// A ClassReference specifies a resource class that will be used to
// dynamically provision a managed resource when the resource claim is
// created.
// +optional
ClassReference *corev1.ObjectReference `json:"classRef,omitempty"`
// A ResourceReference specifies an existing managed resource, in any
// namespace, to which this resource claim should attempt to bind. Omit the
// resource reference to enable dynamic provisioning using a resource class;
// the resource reference will be automatically populated by Crossplane.
// +optional
ResourceReference *corev1.ObjectReference `json:"resourceRef,omitempty"`
}
// A ResourceClaimStatus represents the observed status of a resource claim.
// Deprecated. See https://github.com/crossplane/crossplane/issues/1670
type ResourceClaimStatus struct {
ConditionedStatus `json:",inline"`
BindingStatus `json:",inline"`
}
// TODO(negz): Rename Resource* to Managed* to clarify that they enable the
// resource.Managed interface.
@ -183,20 +132,6 @@ type ResourceSpec struct {
// +optional
WriteConnectionSecretToReference *SecretReference `json:"writeConnectionSecretToRef,omitempty"`
// ClaimReference specifies the resource claim to which this managed
// resource will be bound. ClaimReference is set automatically during
// dynamic provisioning.
// Deprecated. See https://github.com/crossplane/crossplane/issues/1670
//
// +optional
ClaimReference *corev1.ObjectReference `json:"claimRef,omitempty"`
// ClassReference specifies the resource class that was used to dynamically
// provision this managed resource, if any.
// Deprecated. See https://github.com/crossplane/crossplane/issues/1670
// +optional
ClassReference *corev1.ObjectReference `json:"classRef,omitempty"`
// ProviderConfigReference specifies how the provider that will be used to
// create, observe, update, and delete this managed resource should be
// configured.
@ -215,59 +150,11 @@ type ResourceSpec struct {
// +optional
// +kubebuilder:validation:Enum=Orphan;Delete
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
// ReclaimPolicy specifies what will happen to this managed resource when
// its resource claim is deleted, and what will happen to the underlying
// external resource when the managed resource is deleted. The "Delete"
// policy causes the managed resource to be deleted when its bound resource
// claim is deleted, and in turn causes the external resource to be deleted
// when its managed resource is deleted. The "Retain" policy causes the
// managed resource to be retained, in binding phase "Released", when its
// resource claim is deleted, and in turn causes the external resource to be
// retained when its managed resource is deleted. The "Delete" policy is
// used when no policy is specified.
//
// Deprecated. DeletionPolicy takes precedence when both are set.
// See https://github.com/crossplane/crossplane-runtime/issues/179.
//
// +optional
// +kubebuilder:validation:Enum=Retain;Delete
ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"`
}
// ResourceStatus represents the observed state of a managed resource.
type ResourceStatus struct {
ConditionedStatus `json:",inline"`
BindingStatus `json:",inline"`
}
// A ClassSpecTemplate defines a template that will be used to create the
// specifications of managed resources dynamically provisioned using a resource
// class.
type ClassSpecTemplate struct {
// WriteConnectionSecretsToNamespace specifies the namespace in which the
// connection secrets of managed resources dynamically provisioned using
// this claim will be created.
WriteConnectionSecretsToNamespace string `json:"writeConnectionSecretsToNamespace"`
// ProviderReference specifies the provider that will be used to create,
// observe, update, and delete managed resources that are dynamically
// provisioned using this resource class.
ProviderReference Reference `json:"providerRef"`
// ReclaimPolicy specifies what will happen to managed resources dynamically
// provisioned using this class when their resource claims are deleted, and
// what will happen to their underlying external resource when they are
// deleted. The "Delete" policy causes the managed resource to be deleted
// when its bound resource claim is deleted, and in turn causes the external
// resource to be deleted when its managed resource is deleted. The "Retain"
// policy causes the managed resource to be retained, in binding phase
// "Released", when its resource claim is deleted, and in turn causes the
// external resource to be retained when its managed resource is deleted.
// The "Delete" policy is used when no policy is specified.
// +optional
// +kubebuilder:validation:Enum=Retain;Delete
ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"`
}
// A ProviderSpec defines the common way to get to the necessary objects to connect

View File

@ -21,41 +21,9 @@ limitations under the License.
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/api/core/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindingStatus) DeepCopyInto(out *BindingStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindingStatus.
func (in *BindingStatus) DeepCopy() *BindingStatus {
if in == nil {
return nil
}
out := new(BindingStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClassSpecTemplate) DeepCopyInto(out *ClassSpecTemplate) {
*out = *in
out.ProviderReference = in.ProviderReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClassSpecTemplate.
func (in *ClassSpecTemplate) DeepCopy() *ClassSpecTemplate {
if in == nil {
return nil
}
out := new(ClassSpecTemplate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Condition) DeepCopyInto(out *Condition) {
*out = *in
@ -164,58 +132,6 @@ func (in *Reference) DeepCopy() *Reference {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceClaimSpec) DeepCopyInto(out *ResourceClaimSpec) {
*out = *in
if in.WriteConnectionSecretToReference != nil {
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
*out = new(LocalSecretReference)
**out = **in
}
if in.ClassSelector != nil {
in, out := &in.ClassSelector, &out.ClassSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.ClassReference != nil {
in, out := &in.ClassReference, &out.ClassReference
*out = new(corev1.ObjectReference)
**out = **in
}
if in.ResourceReference != nil {
in, out := &in.ResourceReference, &out.ResourceReference
*out = new(corev1.ObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceClaimSpec.
func (in *ResourceClaimSpec) DeepCopy() *ResourceClaimSpec {
if in == nil {
return nil
}
out := new(ResourceClaimSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceClaimStatus) DeepCopyInto(out *ResourceClaimStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
out.BindingStatus = in.BindingStatus
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceClaimStatus.
func (in *ResourceClaimStatus) DeepCopy() *ResourceClaimStatus {
if in == nil {
return nil
}
out := new(ResourceClaimStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
*out = *in
@ -224,16 +140,6 @@ func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
*out = new(SecretReference)
**out = **in
}
if in.ClaimReference != nil {
in, out := &in.ClaimReference, &out.ClaimReference
*out = new(corev1.ObjectReference)
**out = **in
}
if in.ClassReference != nil {
in, out := &in.ClassReference, &out.ClassReference
*out = new(corev1.ObjectReference)
**out = **in
}
if in.ProviderConfigReference != nil {
in, out := &in.ProviderConfigReference, &out.ProviderConfigReference
*out = new(Reference)
@ -260,7 +166,6 @@ func (in *ResourceSpec) DeepCopy() *ResourceSpec {
func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
out.BindingStatus = in.BindingStatus
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus.
@ -341,7 +246,7 @@ func (in *TargetSpec) DeepCopyInto(out *TargetSpec) {
}
if in.ResourceReference != nil {
in, out := &in.ResourceReference, &out.ResourceReference
*out = new(corev1.ObjectReference)
*out = new(v1.ObjectReference)
**out = **in
}
}

View File

@ -1,259 +0,0 @@
/*
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 claimbinding
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"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// Error strings.
const (
errCreateManaged = "cannot create managed resource"
errUpdateClaim = "cannot update resource claim"
errUpdateManaged = "cannot update managed resource"
errUpdateManagedStatus = "cannot update managed resource status"
errDeleteManaged = "cannot delete managed resource"
errBindMismatch = "refusing to bind to managed resource that does not reference resource claim"
errUnbindMismatch = "refusing to 'unbind' from managed resource that does not reference resource claim"
errBindControlled = "refusing to bind to managed resource that is controlled by another resource"
)
// 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 resource.Claim, cs resource.Class, mg resource.Managed) error {
cmr := meta.ReferenceTo(cm, resource.MustGetKind(cm, a.typer))
csr := meta.ReferenceTo(cs, resource.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, resource.MustGetKind(mg, a.typer))
cm.SetResourceReference(mgr)
return errors.Wrap(a.client.Update(ctx, cm), errUpdateClaim)
}
// 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 resource.Claim, mg resource.Managed) error {
// A managed resource that was statically provisioned by an infrastructure
// operator should not have a controller reference. We assume a managed
// resource with a controller reference is part of a composite resource or a
// stack, and therefore not available to be claimed.
if metav1.GetControllerOf(mg) != nil {
return errors.New(errBindControlled)
}
// Note that we allow a claim to bind to a managed resource with a nil claim
// reference in order to enable the static provisioning case in which a
// managed resource is provisioned ahead of time and is not associated with
// any claim.
if r := mg.GetClaimReference(); r != nil && !equal(meta.ReferenceTo(cm, resource.MustGetKind(cm, a.typer)), r) {
return errors.New(errBindMismatch)
}
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, resource.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 by removing the
// managed resource's claim reference, transitioning it to binding phase
// "Released", and if the managed resource's reclaim policy is "Delete",
// deleting it.
func (a *APIBinder) Unbind(ctx context.Context, cm resource.Claim, mg resource.Managed) error {
if !equal(meta.ReferenceTo(cm, resource.MustGetKind(cm, a.typer)), mg.GetClaimReference()) {
return errors.New(errUnbindMismatch)
}
mg.SetBindingPhase(v1alpha1.BindingPhaseReleased)
mg.SetClaimReference(nil)
if err := a.client.Update(ctx, mg); err != nil {
return errors.Wrap(resource.IgnoreNotFound(err), errUpdateManaged)
}
// We go to the trouble of unbinding the managed resource before deleting it
// because we want it to show up as "released" (not "bound") if its managed
// resource reconciler is wedged or delayed trying to delete it.
if mg.GetReclaimPolicy() != v1alpha1.ReclaimDelete {
return nil
}
return errors.Wrap(resource.IgnoreNotFound(a.client.Delete(ctx, mg)), errDeleteManaged)
}
// 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 resource.Claim, mg resource.Managed) error {
// A managed resource that was statically provisioned by an infrastructure
// operator should not have a controller reference. We assume a managed
// resource with a controller reference is part of a composite resource or a
// stack, and therefore not available to be claimed.
if metav1.GetControllerOf(mg) != nil {
return errors.New(errBindControlled)
}
// Note that we allow a claim to bind to a managed resource with a nil claim
// reference in order to enable the static provisioning case in which a
// managed resource is provisioned ahead of time and is not associated with
// any claim.
if r := mg.GetClaimReference(); r != nil && !equal(meta.ReferenceTo(cm, resource.MustGetKind(cm, a.typer)), r) {
return errors.New(errBindMismatch)
}
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, resource.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 by removing the
// managed resource's claim reference, transitioning it to binding phase
// "Released", and if the managed resource's reclaim policy is "Delete",
// deleting it.
func (a *APIStatusBinder) Unbind(ctx context.Context, cm resource.Claim, mg resource.Managed) error {
if !equal(meta.ReferenceTo(cm, resource.MustGetKind(cm, a.typer)), mg.GetClaimReference()) {
return errors.New(errUnbindMismatch)
}
mg.SetClaimReference(nil)
if err := a.client.Update(ctx, mg); err != nil {
return errors.Wrap(resource.IgnoreNotFound(err), errUpdateManaged)
}
mg.SetBindingPhase(v1alpha1.BindingPhaseReleased)
if err := a.client.Status().Update(ctx, mg); err != nil {
return errors.Wrap(resource.IgnoreNotFound(err), errUpdateManagedStatus)
}
// We go to the trouble of unbinding the managed resource before deleting it
// because we want it to show up as "released" (not "bound") if its managed
// resource reconciler is wedged or delayed trying to delete it.
if mg.GetReclaimPolicy() != v1alpha1.ReclaimDelete {
return nil
}
return errors.Wrap(resource.IgnoreNotFound(a.client.Delete(ctx, mg)), errDeleteManaged)
}
// equal returns true if the supplied object references are considered equal. We
// consider two otherwise matching references with different UIDs to be equal,
// presuming that they are both references to a particular object that has been
// deleted and recreated, e.g. due to being restored from a backup.
//
// TODO(negz): If we switch to a reference that only has the fields we care
// about (GVK, namespace, and name) we can just use struct equality.
// https://github.com/crossplane/crossplane-runtime/issues/49
func equal(a, b *corev1.ObjectReference) bool {
switch {
case a == nil || b == nil:
return a == b
case a.APIVersion != b.APIVersion:
return false
case a.Kind != b.Kind:
return false
case a.Namespace != b.Namespace:
return false
}
return a.Name == b.Name
}

View File

@ -1,919 +0,0 @@
/*
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 claimbinding
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var (
_ ManagedCreator = &APIManagedCreator{}
_ Binder = &APIBinder{}
_ Binder = &APIStatusBinder{}
)
func TestCreate(t *testing.T) {
type fields struct {
client client.Client
typer runtime.ObjectTyper
}
type args struct {
ctx context.Context
cm resource.Claim
cs resource.Class
mg resource.Managed
}
cmname := "coolclaim"
csname := "coolclass"
mgname := "coolmanaged"
errBoom := errors.New("boom")
cases := map[string]struct {
fields fields
args args
want error
}{
"CreateManagedError": {
fields: fields{
client: &test.MockClient{
MockCreate: test.NewMockCreateFn(errBoom),
},
typer: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: errors.Wrap(errBoom, errCreateManaged),
},
"UpdateClaimError": {
fields: fields{
client: &test.MockClient{
MockCreate: test.NewMockCreateFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
typer: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: errors.Wrap(errBoom, errUpdateClaim),
},
"Successful": {
fields: fields{
client: &test.MockClient{
MockCreate: test.NewMockCreateFn(nil, func(got runtime.Object) error {
want := &fake.Managed{}
want.SetName(mgname)
want.SetClaimReference(&corev1.ObjectReference{
Name: cmname,
APIVersion: fake.GVK(&fake.Claim{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Claim{}).Kind,
})
want.SetClassReference(&corev1.ObjectReference{
Name: csname,
APIVersion: fake.GVK(&fake.Class{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Class{}).Kind,
})
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetName(cmname)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
APIVersion: fake.GVK(&fake.Managed{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Managed{}).Kind,
})
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
typer: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: cmname}},
cs: &fake.Class{ObjectMeta: metav1.ObjectMeta{Name: csname}},
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Name: mgname}},
},
want: nil,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIManagedCreator(tc.fields.client, tc.fields.typer)
err := api.Create(tc.args.ctx, tc.args.cm, tc.args.cs, tc.args.mg)
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Create(...): -want error, +got error:\n%s", diff)
}
})
}
}
func TestBind(t *testing.T) {
type args struct {
ctx context.Context
cm resource.Claim
mg resource.Managed
}
type want struct {
err error
cm resource.Claim
mg resource.Managed
}
errBoom := errors.New("boom")
externalName := "very-cool-external-resource"
controller := true
cases := map[string]struct {
client client.Client
typer runtime.ObjectTyper
args args
want want
}{
"ControlledError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{{Controller: &controller}},
}},
},
want: want{
err: errors.New(errBindControlled),
cm: &fake.Claim{},
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{{Controller: &controller}},
}},
},
},
"RefMismatchError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
want: want{
err: errors.New(errBindMismatch),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
},
"UpdateManagedError": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
switch obj.(type) {
case *fake.Managed:
return errBoom
default:
return errors.New("unexpected object kind")
}
})},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManaged),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"UpdateClaimError": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
switch obj.(type) {
case *fake.Managed:
return nil
case *fake.Claim:
return errBoom
default:
return errors.New("unexpected object kind")
}
})},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
},
},
want: want{
err: errors.Wrap(errBoom, errUpdateClaim),
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"SuccessfulWithoutExternalName": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{},
},
want: want{
err: nil,
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"SuccessfulWithExternalName": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
},
},
want: want{
err: nil,
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIBinder(tc.client, tc.typer)
err := api.Bind(tc.args.ctx, tc.args.cm, tc.args.mg)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Bind(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.cm, tc.args.cm, test.EquateConditions()); diff != "" {
t.Errorf("api.Bind(...) Claim: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg, test.EquateConditions()); diff != "" {
t.Errorf("api.Bind(...) Managed: -want, +got:\n%s", diff)
}
})
}
}
func TestStatusBind(t *testing.T) {
type args struct {
ctx context.Context
cm resource.Claim
mg resource.Managed
}
type want struct {
err error
cm resource.Claim
mg resource.Managed
}
errBoom := errors.New("boom")
externalName := "very-cool-external-resource"
controller := true
cases := map[string]struct {
client client.Client
typer runtime.ObjectTyper
args args
want want
}{
"ControlledError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{{Controller: &controller}},
}},
},
want: want{
err: errors.New(errBindControlled),
cm: &fake.Claim{},
mg: &fake.Managed{ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{{Controller: &controller}},
}},
},
},
"RefMismatchError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
want: want{
err: errors.New(errBindMismatch),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
},
"UpdateManagedError": {
client: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
switch obj.(type) {
case *fake.Managed:
return errBoom
default:
return errors.New("unexpected object kind")
}
})},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManaged),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
},
},
},
"UpdateManagedStatusError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(errBoom),
},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManagedStatus),
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"UpdateClaimError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
switch obj.(type) {
case *fake.Managed:
return nil
case *fake.Claim:
return errBoom
default:
return errors.New("unexpected object kind")
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
},
},
want: want{
err: errors.Wrap(errBoom, errUpdateClaim),
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"SuccessfulWithoutExternalName": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{},
},
want: want{
err: nil,
cm: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
mg: &fake.Managed{
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
"SuccessfulWithExternalName": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
},
},
want: want{
err: nil,
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName}},
ClaimReferencer: fake.ClaimReferencer{Ref: meta.ReferenceTo(&fake.Claim{}, fake.GVK(&fake.Claim{}))},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIStatusBinder(tc.client, tc.typer)
err := api.Bind(tc.args.ctx, tc.args.cm, tc.args.mg)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Bind(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.cm, tc.args.cm, test.EquateConditions()); diff != "" {
t.Errorf("api.Bind(...) Claim: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg, test.EquateConditions()); diff != "" {
t.Errorf("api.Bind(...) Managed: -want, +got:\n%s", diff)
}
})
}
}
func TestUnbind(t *testing.T) {
type args struct {
ctx context.Context
cm resource.Claim
mg resource.Managed
}
type want struct {
err error
mg resource.Managed
}
errBoom := errors.New("boom")
typer := fake.SchemeWith(&fake.Claim{})
ref := meta.ReferenceTo(&fake.Claim{}, resource.MustGetKind(&fake.Claim{}, typer))
cases := map[string]struct {
client client.Client
typer runtime.ObjectTyper
args args
want want
}{
"RefMismatchError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{ClaimReferencer: fake.ClaimReferencer{Ref: ref}},
},
want: want{
err: errors.New(errUnbindMismatch),
mg: &fake.Managed{ClaimReferencer: fake.ClaimReferencer{Ref: ref}},
},
},
"SuccessfulRetain": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: nil,
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"SuccessfulDelete": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockDelete: test.NewMockDeleteFn(nil),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: nil,
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"UpdateError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(errBoom),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManaged),
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"DeleteError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockDelete: test.NewMockDeleteFn(errBoom),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: errors.Wrap(errBoom, errDeleteManaged),
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIBinder(tc.client, tc.typer)
err := api.Unbind(tc.args.ctx, tc.args.cm, tc.args.mg)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Unbind(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg, test.EquateConditions()); diff != "" {
t.Errorf("api.Unbind(...) Managed: -want, +got:\n%s", diff)
}
})
}
}
func TestStatusUnbind(t *testing.T) {
type args struct {
ctx context.Context
cm resource.Claim
mg resource.Managed
}
type want struct {
err error
mg resource.Managed
}
errBoom := errors.New("boom")
typer := fake.SchemeWith(&fake.Claim{})
ref := meta.ReferenceTo(&fake.Claim{}, resource.MustGetKind(&fake.Claim{}, typer))
cases := map[string]struct {
client client.Client
typer runtime.ObjectTyper
args args
want want
}{
"RefMismatchError": {
typer: fake.SchemeWith(&fake.Claim{}),
args: args{
ctx: context.Background(),
cm: &fake.Claim{ObjectMeta: metav1.ObjectMeta{Name: "I'm different!"}},
mg: &fake.Managed{ClaimReferencer: fake.ClaimReferencer{Ref: ref}},
},
want: want{
err: errors.New(errUnbindMismatch),
mg: &fake.Managed{ClaimReferencer: fake.ClaimReferencer{Ref: ref}},
},
},
"SuccessfulRetain": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: nil,
mg: &fake.Managed{
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"SuccessfulDelete": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
MockDelete: test.NewMockDeleteFn(nil),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: nil,
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"UpdateError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(errBoom),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManaged),
mg: &fake.Managed{
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"UpdateStatusError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(errBoom),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: errors.Wrap(errBoom, errUpdateManagedStatus),
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
"DeleteError": {
client: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
MockDelete: test.NewMockDeleteFn(errBoom),
},
typer: typer,
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound},
ClaimReferencer: fake.ClaimReferencer{Ref: ref},
},
},
want: want{
err: errors.Wrap(errBoom, errDeleteManaged),
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased},
ClaimReferencer: fake.ClaimReferencer{Ref: nil},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
api := NewAPIStatusBinder(tc.client, tc.typer)
err := api.Unbind(tc.args.ctx, tc.args.cm, tc.args.mg)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("api.Unbind(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg, test.EquateConditions()); diff != "" {
t.Errorf("api.Unbind(...) Managed: -want, +got:\n%s", diff)
}
})
}
}
func TestObjectReferenceEqual(t *testing.T) {
cases := map[string]struct {
a *corev1.ObjectReference
b *corev1.ObjectReference
want bool
}{
"BothNil": {
want: true,
},
"OneIsNil": {
a: &corev1.ObjectReference{},
want: false,
},
"MismatchedAPIVersion": {
a: &corev1.ObjectReference{
APIVersion: "v",
},
b: &corev1.ObjectReference{},
want: false,
},
"MismatchedKind": {
a: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
},
b: &corev1.ObjectReference{
APIVersion: "v",
},
want: false,
},
"MismatchedNamespace": {
a: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
Namespace: "ns",
},
b: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
},
want: false,
},
"MismatchedName": {
a: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
Namespace: "ns",
Name: "cool",
},
b: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
Namespace: "ns",
},
want: false,
},
"Match": {
a: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
Namespace: "ns",
Name: "cool",
},
b: &corev1.ObjectReference{
APIVersion: "v",
Kind: "k",
Namespace: "ns",
Name: "cool",
},
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := equal(tc.a, tc.b)
if got != tc.want {
t.Errorf("equal(...): want %t, got %t", tc.want, got)
}
})
}
}

View File

@ -1,91 +0,0 @@
/*
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 claimbinding
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
// A ConfiguratorChain chains multiple configurators.
type ConfiguratorChain []ManagedConfigurator
// Configure calls each ManagedConfigurator serially. It returns the first
// error it encounters, if any.
func (cc ConfiguratorChain) Configure(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error {
for _, c := range cc {
if err := c.Configure(ctx, cm, cs, mg); err != nil {
return err
}
}
return nil
}
// An ObjectMetaConfigurator sets standard object metadata for a dynamically
// provisioned resource, deriving it from a class and claim. It is deprecated;
// use ConfigureNames instead.
type ObjectMetaConfigurator struct{}
// NewObjectMetaConfigurator returns a new ObjectMetaConfigurator.
func NewObjectMetaConfigurator(_ runtime.ObjectTyper) *ObjectMetaConfigurator {
return &ObjectMetaConfigurator{}
}
// Configure the supplied Managed resource's object metadata.
func (c *ObjectMetaConfigurator) Configure(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error {
return ConfigureNames(ctx, cm, cs, mg)
}
// ConfigureNames configures the name and external name of the supplied managed
// resource. The managed resource name is derived from the supplied resource
// claim, in the form {claim-namespace}-{claim-name}-{random-string}. The
// resource claim's external name annotation, if any, is propagated to the
// managed resource.
func ConfigureNames(_ context.Context, cm resource.Claim, _ resource.Class, mg resource.Managed) error {
mg.SetGenerateName(fmt.Sprintf("%s-%s-", cm.GetNamespace(), cm.GetName()))
if meta.GetExternalName(cm) != "" {
meta.SetExternalName(mg, meta.GetExternalName(cm))
}
return nil
}
// ConfigureReclaimPolicy configures the reclaim policy of the supplied managed
// resource. If the managed resource _already has_ a reclaim policy (for example
// because one was set by another configurator) it is respected. Otherwise the
// reclaim policy is copied from the resource class. If the resource class does
// not specify a reclaim policy, the managed resource's policy is set to
// "Delete".
func ConfigureReclaimPolicy(_ context.Context, _ resource.Claim, cs resource.Class, mg resource.Managed) error {
if mg.GetReclaimPolicy() != "" {
return nil
}
mg.SetReclaimPolicy(cs.GetReclaimPolicy())
if mg.GetReclaimPolicy() == "" {
mg.SetReclaimPolicy(v1alpha1.ReclaimDelete)
}
return nil
}

View File

@ -1,247 +0,0 @@
/*
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 claimbinding
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var (
_ ManagedConfiguratorFn = ConfigureNames
_ ManagedConfiguratorFn = ConfigureReclaimPolicy
_ ManagedConfigurator = ConfiguratorChain{}
)
func TestConfiguratorChain(t *testing.T) {
errBoom := errors.New("boom")
type args struct {
ctx context.Context
cm resource.Claim
cs resource.Class
mg resource.Managed
}
cases := map[string]struct {
cc ConfiguratorChain
args args
want error
}{
"EmptyChain": {
cc: ConfiguratorChain{},
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: nil,
},
"SuccessulConfigurator": {
cc: ConfiguratorChain{
ManagedConfiguratorFn(func(_ context.Context, _ resource.Claim, _ resource.Class, _ resource.Managed) error {
return nil
}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: nil,
},
"ConfiguratorReturnsError": {
cc: ConfiguratorChain{
ManagedConfiguratorFn(func(_ context.Context, _ resource.Claim, _ resource.Class, _ resource.Managed) error {
return errBoom
}),
},
args: args{
ctx: context.Background(),
cm: &fake.Claim{},
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: errBoom,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := tc.cc.Configure(tc.args.ctx, tc.args.cm, tc.args.cs, tc.args.mg)
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
t.Errorf("tc.cc.Configure(...): -want error, +got error:\n%s", diff)
}
})
}
}
func TestNameConfigurators(t *testing.T) {
claimName := "myclaim"
claimNS := "myclaimns"
externalName := "wayout"
type args struct {
ctx context.Context
cm resource.Claim
cs resource.Class
mg resource.Managed
}
type want struct {
err error
mg resource.Managed
}
cases := map[string]struct {
args args
want want
}{
"Successful": {
args: args{
ctx: context.Background(),
cm: &fake.Claim{
ObjectMeta: metav1.ObjectMeta{
Namespace: claimNS,
Name: claimName,
Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName},
}},
mg: &fake.Managed{},
},
want: want{
mg: &fake.Managed{
ObjectMeta: metav1.ObjectMeta{
GenerateName: claimNS + "-" + claimName + "-",
Annotations: map[string]string{meta.AnnotationKeyExternalName: externalName},
},
},
},
},
}
t.Run("TestConfigureNames", func(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := ConfigureNames(tc.args.ctx, tc.args.cm, tc.args.cs, tc.args.mg)
if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" {
t.Errorf("ConfigureNames(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg); diff != "" {
t.Errorf("ConfigureNames(...) Managed: -want, +got error:\n%s", diff)
}
})
}
})
// NOTE(negz): This deprecated API wraps ConfigureNames; they should behave
// identically.
t.Run("TestObjectMetaConfigurator", func(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
om := NewObjectMetaConfigurator(nil)
got := om.Configure(tc.args.ctx, tc.args.cm, tc.args.cs, tc.args.mg)
if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" {
t.Errorf("om.Configure(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg); diff != "" {
t.Errorf("om.Configure(...) Managed: -want, +got error:\n%s", diff)
}
})
}
})
}
func TestConfigureReclaimPolicy(t *testing.T) {
type args struct {
ctx context.Context
cm resource.Claim
cs resource.Class
mg resource.Managed
}
type want struct {
err error
mg resource.Managed
}
cases := map[string]struct {
reason string
args args
want want
}{
"AlreadySet": {
reason: "Existing managed resource reclaim policies should be respected.",
args: args{
ctx: context.Background(),
cs: &fake.Class{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete}},
mg: &fake.Managed{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain}},
},
want: want{
mg: &fake.Managed{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain}},
},
},
"SetByClass": {
reason: "The class's reclaim policy should be propagated to the managed resource.",
args: args{
ctx: context.Background(),
cs: &fake.Class{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain}},
mg: &fake.Managed{},
},
want: want{
mg: &fake.Managed{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain}},
},
},
"DefaultToDelete": {
reason: "If neither the class nor managed resource set a reclaim policy, it should default to Delete.",
args: args{
ctx: context.Background(),
cs: &fake.Class{},
mg: &fake.Managed{},
},
want: want{
mg: &fake.Managed{Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete}},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := ConfigureReclaimPolicy(tc.args.ctx, tc.args.cm, tc.args.cs, tc.args.mg)
if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" {
t.Errorf("\nReason: %s\nConfigureReclaimPolicy(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.mg, tc.args.mg); diff != "" {
t.Errorf("\nReason: %s\nConfigureReclaimPolicy(...) Managed: -want, +got error:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -1,19 +0,0 @@
/*
Copyright 2020 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 claimbinding provides a resource claim binding reconciler.
// Deprecated: See https://github.com/crossplane/crossplane/issues/1670
package claimbinding

View File

@ -1,514 +0,0 @@
/*
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 claimbinding
import (
"context"
"strings"
"time"
"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/schema"
"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/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
claimFinalizerName = "finalizer.resourceclaim.crossplane.io"
claimReconcileTimeout = 1 * time.Minute
aShortWait = 30 * time.Second
)
// Reasons a resource claim is or is not ready.
const (
ReasonBinding = "Managed claim is waiting for managed resource to become bindable"
)
// Error strings.
const (
errGetClaim = "cannot get resource claim"
errUpdateClaimStatus = "cannot update resource claim status"
)
// Event reasons.
const (
reasonCannotGetResource event.Reason = "CannotGetManagedResource"
reasonCannotGetClass event.Reason = "CannotGetResourceClass"
reasonCannotConfigureResource event.Reason = "CannotConfigureManagedResource"
reasonCannotCreateResource event.Reason = "CannotCreateManagedResource"
reasonCannotPropagate event.Reason = "CannotPropagateConnectionDetails"
reasonCannotBind event.Reason = "CannotBindManagedResource"
reasonCannotUnbind event.Reason = "CannotUnbindManagedResource"
reasonResourceNotFound event.Reason = "ManagedResourceNotFound"
reasonCreatedResource event.Reason = "CreatedManagedResource"
reasonWaitingToBind event.Reason = "WaitingToBind"
reasonBound event.Reason = "BoundManagedResource"
reasonUnbound event.Reason = "UnboundManagedResource"
)
// ControllerName returns the recommended name for controllers that use this
// package to reconcile a particular kind of resource claim.
func ControllerName(kind string) string {
return "claimbinding/" + strings.ToLower(kind)
}
// A ManagedConfigurator configures a resource, typically by converting it to
// a known type and populating its spec.
type ManagedConfigurator interface {
Configure(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error
}
// A ManagedConfiguratorFn is a function that satisfies the
// ManagedConfigurator interface.
type ManagedConfiguratorFn func(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error
// Configure the supplied resource using the supplied claim and class.
func (fn ManagedConfiguratorFn) Configure(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error {
return fn(ctx, cm, cs, mg)
}
// A ManagedCreator creates a resource, typically by submitting it to an API
// server. ManagedCreators must not modify the supplied resource class, but are
// responsible for final modifications to the claim and resource, for example
// ensuring resource, class, claim, and owner references are set.
type ManagedCreator interface {
Create(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error
}
// A ManagedCreatorFn is a function that satisfies the ManagedCreator interface.
type ManagedCreatorFn func(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error
// Create the supplied resource.
func (fn ManagedCreatorFn) Create(ctx context.Context, cm resource.Claim, cs resource.Class, mg resource.Managed) error {
return fn(ctx, cm, cs, mg)
}
// A Binder binds a resource claim to a managed resource.
type Binder interface {
// Bind the supplied Claim to the supplied Managed resource.
Bind(ctx context.Context, cm resource.Claim, mg resource.Managed) error
// Unbind the supplied Claim from the supplied Managed resource.
Unbind(ctx context.Context, cm resource.Claim, mg resource.Managed) error
}
// BinderFns satisfy the Binder interface.
type BinderFns struct {
BindFn func(ctx context.Context, cm resource.Claim, mg resource.Managed) error
UnbindFn func(ctx context.Context, cm resource.Claim, mg resource.Managed) error
}
// Bind the supplied Claim to the supplied Managed resource.
func (b BinderFns) Bind(ctx context.Context, cm resource.Claim, mg resource.Managed) error {
return b.BindFn(ctx, cm, mg)
}
// Unbind the supplied Claim from the supplied Managed resource.
func (b BinderFns) Unbind(ctx context.Context, cm resource.Claim, mg resource.Managed) error {
return b.UnbindFn(ctx, cm, mg)
}
// A Reconciler reconciles resource claims by creating exactly one kind of
// concrete managed resource. Each resource claim kind should create an instance
// of this controller for each managed resource kind they can bind to, using
// watch predicates to ensure each controller is responsible for exactly one
// type of resource class provisioner. Each controller must watch its subset of
// resource claims and any managed resources they control.
type Reconciler struct {
client client.Client
newClaim func() resource.Claim
newClass func() resource.Class
newManaged func() resource.Managed
// The below structs embed the set of interfaces used to implement the
// resource claim reconciler. We do this primarily for readability, so that
// the reconciler logic reads r.managed.Create(), r.claim.Finalize(), etc.
managed crManaged
claim crClaim
log logging.Logger
record event.Recorder
}
type crManaged struct {
ManagedConfigurator
ManagedCreator
resource.ManagedConnectionPropagator
}
func defaultCRManaged(m manager.Manager) crManaged {
return crManaged{
ManagedConfigurator: ConfiguratorChain{
ManagedConfiguratorFn(ConfigureNames),
ManagedConfiguratorFn(ConfigureReclaimPolicy),
},
ManagedCreator: NewAPIManagedCreator(m.GetClient(), m.GetScheme()),
// TODO(negz): Switch to ConnectionPropagator once this has been
// deprecated for a release or two.
//nolint:staticcheck
ManagedConnectionPropagator: resource.NewAPIManagedConnectionPropagator(m.GetClient(), m.GetScheme()),
}
}
type crClaim struct {
resource.Finalizer
Binder
}
func defaultCRClaim(m manager.Manager) crClaim {
return crClaim{
Finalizer: resource.NewAPIFinalizer(m.GetClient(), claimFinalizerName),
Binder: NewAPIStatusBinder(m.GetClient(), m.GetScheme()),
}
}
// A ReconcilerOption configures a Reconciler.
type ReconcilerOption func(*Reconciler)
// WithManagedConfigurators specifies which configurators should be used to
// configure each managed resource. Configurators will be applied in the order
// they are specified.
func WithManagedConfigurators(c ...ManagedConfigurator) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ManagedConfigurator = ConfiguratorChain(c)
}
}
// WithManagedCreator specifies which ManagedCreator should be used to create
// managed resources.
func WithManagedCreator(c ManagedCreator) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ManagedCreator = c
}
}
// WithManagedConnectionPropagator specifies which ManagedConnectionPropagator
// should be used to propagate resource connection details to their claim.
//
// Deprecated: Use WithConnectionPropagator.
func WithManagedConnectionPropagator(p resource.ManagedConnectionPropagator) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ManagedConnectionPropagator = p
}
}
// WithConnectionPropagator specifies which ConnectionPropagator
// should be used to propagate resource connection details to their claim.
func WithConnectionPropagator(p resource.ConnectionPropagator) ReconcilerOption {
return func(r *Reconciler) {
r.managed.ManagedConnectionPropagator = &resource.APIManagedConnectionPropagator{Propagator: p}
}
}
// WithBinder specifies which Binder should be used to bind
// resources to their claim.
func WithBinder(b Binder) ReconcilerOption {
return func(r *Reconciler) {
r.claim.Binder = b
}
}
// WithClaimFinalizer specifies which ClaimFinalizer should be used to finalize
// claims when they are deleted.
func WithClaimFinalizer(f resource.Finalizer) ReconcilerOption {
return func(r *Reconciler) {
r.claim.Finalizer = f
}
}
// 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
}
}
// NewReconciler returns a Reconciler that reconciles resource claims
// of the supplied ClaimKind with resources of the supplied ManagedKind. It
// panics if asked to reconcile a claim or resource kind that is not registered
// with the supplied manager's runtime.Scheme. The returned Reconciler will
// apply only the ObjectMetaConfigurator by default; most callers should supply
// one or more ManagedConfigurators to configure their managed resources.
func NewReconciler(m manager.Manager, of resource.ClaimKind, using resource.ClassKind, with resource.ManagedKind, o ...ReconcilerOption) *Reconciler {
nc := func() resource.Claim {
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Claim)
}
ns := func() resource.Class {
return resource.MustCreateObject(schema.GroupVersionKind(using), m.GetScheme()).(resource.Class)
}
nr := func() resource.Managed {
return resource.MustCreateObject(schema.GroupVersionKind(with), m.GetScheme()).(resource.Managed)
}
// Panic early if we've been asked to reconcile a claim or resource kind
// that has not been registered with our controller manager's scheme.
_, _, _ = nc(), ns(), nr()
r := &Reconciler{
client: m.GetClient(),
newClaim: nc,
newClass: ns,
newManaged: nr,
managed: defaultCRManaged(m),
claim: defaultCRClaim(m),
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a resource claim with a concrete managed resource.
func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) { // nolint:gocyclo
// NOTE(negz): This method is well over our cyclomatic complexity goal.
// Be wary of adding additional complexity.
log := r.log.WithValues("request", req)
log.Debug("Reconciling")
ctx, cancel := context.WithTimeout(context.Background(), claimReconcileTimeout)
defer cancel()
claim := r.newClaim()
if err := r.client.Get(ctx, req.NamespacedName, claim); 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 resource claim", "error", err)
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetClaim)
}
record := r.record.WithAnnotations("external-name", meta.GetExternalName(claim))
log = log.WithValues(
"uid", claim.GetUID(),
"version", claim.GetResourceVersion(),
"external-name", meta.GetExternalName(claim),
)
managed := r.newManaged()
if ref := claim.GetResourceReference(); ref != nil {
record = record.WithAnnotations("managed-name", claim.GetResourceReference().Name)
log = log.WithValues("managed-name", claim.GetResourceReference().Name)
err := r.client.Get(ctx, meta.NamespacedNameOf(ref), managed)
if kerrors.IsNotFound(err) {
// If the managed resource we explicitly reference doesn't exist yet
// we want to retry after a brief wait, in case it is created. We
// must explicitly requeue because our EnqueueRequestForClaim
// handler can only enqueue reconciles for managed resources that
// have their claim reference set, so we can't expect to be queued
// implicitly when the managed resource we want to bind to appears.
log.Debug("Referenced managed resource not found", "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Normal(reasonResourceNotFound, "Referenced managed resource not found"))
claim.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
log.Debug("Cannot get referenced managed resource", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotGetResource, err))
claim.SetConditions(v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
}
if meta.WasDeleted(claim) {
log = log.WithValues("deletion-timestamp", claim.GetDeletionTimestamp())
if err := r.claim.Unbind(ctx, claim, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
log.Debug("Cannot unbind claim", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotUnbind, err))
claim.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
log.Debug("Successfully unbound managed resource")
record.Event(claim, event.Normal(reasonUnbound, "Successfully unbound managed resource"))
if err := r.claim.RemoveFinalizer(ctx, claim); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
log.Debug("Cannot remove finalizer", "error", err, "requeue-after", time.Now().Add(aShortWait))
claim.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// We've successfully deleted our claim and removed our finalizer. If we
// assume we were the only controller that added a finalizer to this
// claim then it should no longer exist and thus there is no point
// trying to update its status.
log.Debug("Successfully deleted resource claim")
return reconcile.Result{Requeue: false}, nil
}
if err := r.claim.AddFinalizer(ctx, claim); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
log.Debug("Cannot add resource claim finalizer", "error", err, "requeue-after", time.Now().Add(aShortWait))
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// Claim reconcilers (should) watch for either claims with a resource ref,
// claims with a class ref, or managed resources with a claim ref. In the
// first case the managed resource always exists by the time we get here. In
// the second case the class reference is set. The third case exposes us to
// a pathological scenario in which a managed resource references a claim
// that has no resource ref or class ref, so we can't assume the class ref
// is always set at this point.
if !meta.WasCreated(managed) && claim.GetClassReference() != nil {
record = record.WithAnnotations("class-name", claim.GetClassReference().Name)
log = log.WithValues("class-name", claim.GetClassReference().Name)
class := r.newClass()
// Class reference should always be set by the time we get this far; we
// set it on last reconciliation.
if err := r.client.Get(ctx, meta.NamespacedNameOf(claim.GetClassReference()), class); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error or the
// class is (re)created.
log.Debug("Cannot get referenced resource class", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotGetClass, err))
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.managed.Configure(ctx, claim, class, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error or some
// issue with the resource class was resolved.
log.Debug("Cannot configure managed resource", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotConfigureResource, err))
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// We'll know our managed resource's name at this point because it was
// set by the above configure step.
record = record.WithAnnotations("managed-name", managed.GetName())
log = log.WithValues("managed-name", managed.GetName())
if err := r.managed.Create(ctx, claim, class, managed); err != nil {
// If we didn't hit this error last time we'll be requeued
// implicitly due to the status update. Otherwise we want to retry
// after a brief wait, in case this was a transient error.
log.Debug("Cannot create managed resource", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotCreateResource, err))
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
log.Debug("Successfully created managed resource")
record.Event(claim, event.Normal(reasonCreatedResource, "Successfully created managed resource"))
}
if !resource.IsBindable(managed) && !resource.IsBound(managed) {
log.Debug("Managed resource is not yet bindable")
record.Event(claim, event.Normal(reasonWaitingToBind, "Managed resource is not yet bindable"))
if managed.GetClaimReference() == nil {
// We're waiting to bind to a statically provisioned managed
// resource. We must requeue because our EnqueueRequestForClaim
// handler can only enqueue reconciles for managed resource updates
// when they have their claim reference set, and that doesn't happen
// until we bind to the managed resource we're waiting for.
claim.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// If this claim was not already binding we'll be requeued due to the
// status update. Otherwise there's no need to requeue. We should be
// watching both the resource claims and the resources we own, so we'll
// be queued if anything changes.
claim.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if resource.IsBindable(managed) {
if err := r.managed.PropagateConnection(ctx, claim, managed); err != nil {
// If we didn't hit this error last time we'll be requeued implicitly
// due to the status update. Otherwise we want to retry after a brief
// wait in case this was a transient error, or the resource connection
// secret is created.
log.Debug("Cannot propagate connection details from managed resource to claim", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotPropagate, err))
claim.SetConditions(Binding(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
if err := r.claim.Bind(ctx, claim, managed); err != nil {
// If we didn't hit this error last time we'll be requeued implicitly
// due to the status update. Otherwise we want to retry after a brief
// wait, in case this was a transient error.
log.Debug("Cannot bind to managed resource", "error", err, "requeue-after", time.Now().Add(aShortWait))
record.Event(claim, event.Warning(reasonCannotBind, err))
claim.SetConditions(Binding(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
log.Debug("Successfully bound managed resource")
record.Event(claim, event.Normal(reasonBound, "Successfully bound managed resource"))
}
// No need to requeue. We should be watching both the resource claims and
// the resources we own, so we'll be queued if anything changes.
claim.SetConditions(v1alpha1.Available(), v1alpha1.ReconcileSuccess())
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
// Binding returns a condition that indicates the resource claim is currently
// waiting for its managed resource to become bindable.
func Binding() v1alpha1.Condition {
return v1alpha1.Condition{
Type: v1alpha1.TypeReady,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: ReasonBinding,
}
}

View File

@ -1,735 +0,0 @@
/*
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 claimbinding
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"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"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &Reconciler{}
func TestReconciler(t *testing.T) {
type args struct {
m manager.Manager
of resource.ClaimKind
use resource.ClassKind
with resource.ManagedKind
o []ReconcilerOption
}
type want struct {
result reconcile.Result
err error
}
errBoom := errors.New("boom")
errUnexpected := errors.New("unexpected object type")
now := metav1.Now()
cases := map[string]struct {
args args
want want
}{
"GetClaimError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"GetManagedError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
return errBoom
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"ManagedNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
return kerrors.NewNotFound(schema.GroupResource{}, "")
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"UnbindError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetDeletionTimestamp(&now)
*o = *cm
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetDeletionTimestamp(&now)
want.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithBinder(BinderFns{UnbindFn: func(_ context.Context, _ resource.Claim, _ resource.Managed) error { return errBoom }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"UnbindSuccess": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetDeletionTimestamp(&now)
*o = *cm
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetDeletionTimestamp(&now)
want.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithBinder(BinderFns{UnbindFn: func(_ context.Context, _ resource.Claim, _ resource.Managed) error { return nil }}),
WithClaimFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: false}},
},
"RemoveClaimFinalizerError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetDeletionTimestamp(&now)
*o = *cm
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetDeletionTimestamp(&now)
want.SetConditions(v1alpha1.Deleting(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithBinder(BinderFns{UnbindFn: func(_ context.Context, _ resource.Claim, _ resource.Managed) error { return nil }}),
WithClaimFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"SuccessfulDelete": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetDeletionTimestamp(&now)
*o = *cm
return nil
default:
return errUnexpected
}
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithBinder(BinderFns{UnbindFn: func(_ context.Context, _ resource.Claim, _ resource.Managed) error { return nil }}),
WithClaimFinalizer(resource.FinalizerFns{RemoveFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ClassReferenceNotSet": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
*o = fake.Claim{}
return nil
case *fake.Managed:
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"GetResourceClassError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Class:
return errBoom
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetClassReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"ConfigureManagedError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Class:
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetClassReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagedConfigurators(ManagedConfiguratorFn(
func(_ context.Context, _ resource.Claim, _ resource.Class, _ resource.Managed) error { return errBoom },
)),
WithClaimFinalizer(resource.FinalizerFns{
AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }},
),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"CreateManagedError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Class:
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetClassReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagedConfigurators(ManagedConfiguratorFn(
func(_ context.Context, _ resource.Claim, _ resource.Class, _ resource.Managed) error { return nil },
)),
WithClaimFinalizer(resource.FinalizerFns{
AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }},
),
WithManagedCreator(ManagedCreatorFn(
func(_ context.Context, _ resource.Claim, _ resource.Class, _ resource.Managed) error { return errBoom },
)),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"ManagedIsInUnknownBindingPhase": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
// We do not explicitly set a BindingPhase here
// because the zero value of BindingPhase is
// BindingPhaseUnset.
mg := &fake.Managed{}
mg.SetClaimReference(&corev1.ObjectReference{})
mg.SetCreationTimestamp(now)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ManagedIsInUnbindableBindingPhase": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
mg := &fake.Managed{}
mg.SetCreationTimestamp(now)
mg.SetClaimReference(&corev1.ObjectReference{})
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbindable)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(Binding(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: false}},
},
"PropagateConnectionError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
mg := &fake.Managed{}
mg.SetCreationTimestamp(now)
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(Binding(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
WithManagedConnectionPropagator(resource.ManagedConnectionPropagatorFn(
func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ resource.Managed) error {
return errBoom
},
)),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"AddFinalizerError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
mg := &fake.Managed{}
mg.SetCreationTimestamp(now)
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagedConnectionPropagator(resource.ManagedConnectionPropagatorFn(
func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ resource.Managed) error { return nil },
)),
WithClaimFinalizer(resource.FinalizerFns{
AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return errBoom }},
),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"BindError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
mg := &fake.Managed{}
mg.SetCreationTimestamp(now)
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(Binding(), v1alpha1.ReconcileError(errBoom))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithManagedConnectionPropagator(resource.ManagedConnectionPropagatorFn(
func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ resource.Managed) error { return nil },
)),
WithClaimFinalizer(resource.FinalizerFns{
AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }},
),
WithBinder(BinderFns{
BindFn: func(_ context.Context, _ resource.Claim, _ resource.Managed) error { return errBoom },
}),
},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"Successful": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *fake.Claim:
cm := &fake.Claim{}
cm.SetResourceReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *fake.Managed:
mg := &fake.Managed{}
mg.SetCreationTimestamp(now)
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
*o = *mg
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetResourceReference(&corev1.ObjectReference{})
want.SetConditions(v1alpha1.Available(), v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}, &fake.Class{}, &fake.Managed{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
use: resource.ClassKind(fake.GVK(&fake.Class{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
o: []ReconcilerOption{
WithClaimFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
},
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, tc.args.of, tc.args.use, tc.args.with, tc.args.o...)
got, err := r.Reconcile(reconcile.Request{})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.result, got); diff != "" {
t.Errorf("r.Reconcile(...): -want, +got:\n%s", diff)
}
})
}
}

View File

@ -1,20 +0,0 @@
/*
Copyright 2020 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 claimdefaulting provides a reconciler that sets the default resource
// class for a resource claim.
// Deprecated: See https://github.com/crossplane/crossplane/issues/1670
package claimdefaulting

View File

@ -1,221 +0,0 @@
/*
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 claimdefaulting
import (
"context"
"math/rand"
"strings"
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"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/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
claimDefaultingReconcileTimeout = 1 * time.Minute
claimDefaultingReconcileMaxJitterMs = 1500
aShortWait = 30 * time.Second
)
// Error strings.
const (
errGetClaim = "cannot get resource claim"
errUpdateClaim = "cannot update resource claim"
errListClasses = "cannot list resource classes"
)
// Event reasons.
const reasonClassFound = "DefaultResourceClass"
// ControllerName returns the recommended name for controllers that use this
// package to reconcile a particular kind of resource claim.
func ControllerName(kind string) string {
return "claimdefaulting/" + strings.ToLower(kind)
}
// A Jitterer sleeps for a random amount of time in order to decrease the chance
// of any one controller predictably winning the race to schedule claims to a
// class, for example because it has fewer classes to list and select from than
// its competitors.
type Jitterer func()
// A Reconciler reconciles resource claims by setting their class reference to
// the resource class annotated as the default. If multiple classes are
// annotated as the default one of the annotated classes will be set, but which
// one is undefined.
type Reconciler struct {
client client.Client
newClaim func() resource.Claim
classKind resource.ClassKind
jitter Jitterer
log logging.Logger
record event.Recorder
}
// A ReconcilerOption configures a Reconciler.
type ReconcilerOption func(*Reconciler)
// WithDefaultingJitterer specifies the Jitterer a Reconciler should use.
func WithDefaultingJitterer(j Jitterer) ReconcilerOption {
return func(r *Reconciler) {
r.jitter = j
}
}
// 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
}
}
// NewReconciler returns a Reconciler that sets the class reference of a
// resource claim to the resource class annotated as the default.
func NewReconciler(m manager.Manager, of resource.ClaimKind, to resource.ClassKind, o ...ReconcilerOption) *Reconciler {
nc := func() resource.Claim {
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Claim)
}
// Panic early if we've been asked to reconcile a claim or resource kind
// that has not been registered with our controller manager's scheme.
_ = nc()
r := &Reconciler{
client: m.GetClient(),
newClaim: nc,
classKind: to,
jitter: func() {
random := rand.New(rand.NewSource(time.Now().UnixNano()))
time.Sleep(time.Duration(random.Intn(claimDefaultingReconcileMaxJitterMs)) * time.Millisecond)
},
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a resource claim by using its class selector to select and allocate
// it a resource class.
func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log := r.log.WithValues("request", req)
log.Debug("Reconciling")
ctx, cancel := context.WithTimeout(context.Background(), claimDefaultingReconcileTimeout)
defer cancel()
claim := r.newClaim()
if err := r.client.Get(ctx, req.NamespacedName, claim); 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 resource claim", "error", err)
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetClaim)
}
record := r.record.WithAnnotations(
"external-name", meta.GetExternalName(claim),
"class-kind", r.classKind.Kind,
)
log = log.WithValues(
"uid", claim.GetUID(),
"version", claim.GetResourceVersion(),
"external-name", meta.GetExternalName(claim),
"class-kind", r.classKind.Kind,
)
// There could be several controllers racing to schedule this claim. If it
// was scheduled since we were queued then another controller won and we
// should abort.
if claim.GetClassReference() != nil {
log.Debug("Resource class is already set")
return reconcile.Result{Requeue: false}, nil
}
classes := &unstructured.UnstructuredList{}
classes.SetGroupVersionKind(r.classKind.List())
if err := r.client.List(ctx, classes); err != nil {
// Claim defaulting controllers don't update the synced status because
// no one scheduler has the full view of whether the process failed or
// succeeded. It's possible another controller can successfully set a
// class even though we can't, so it would be confusing to mark this
// claim as failing to be reconciled. Instead we return an error - we'll
// be requeued but abort immediately if the claim was defaulted.
log.Debug("Cannot list resource classes", "error", err)
return reconcile.Result{}, errors.Wrap(err, errListClasses)
}
defaults := []unstructured.Unstructured{}
for _, c := range classes.Items {
if c.GetAnnotations()[v1alpha1.AnnotationDefaultClassKey] == v1alpha1.AnnotationDefaultClassValue {
defaults = append(defaults, c)
}
}
if len(defaults) == 0 {
// None of our classes are annotated as the default. We can't be sure
// whether another controller owns the default class, or whether there
// is no default class, so we requeue after a short wait. We'll abort
// the next reconcile immediately if another controller defaulted the
// claim.
log.Debug("No default resource classes found", "requeue-after", time.Now().Add(aShortWait))
return reconcile.Result{RequeueAfter: aShortWait}, nil
}
random := rand.New(rand.NewSource(time.Now().UnixNano()))
selected := defaults[random.Intn(len(defaults))]
claim.SetClassReference(meta.ReferenceTo(&selected, schema.GroupVersionKind(r.classKind)))
// There could be several controllers racing to default this claim to a
// class. We sleep for a randomly jittered amount of time before trying to
// update the class reference to decrease the chance of any one controller
// predictably winning the race, for example because it has fewer classes to
// list and select from than its competitors.
r.jitter()
// Attempt to set the class reference. If a competing controller beat us
// we'll fail the write because the claim's resource version has changed
// since we read it. We'll be requeued, but will abort immediately if the
// claim was defaulted.
log.Debug("Attempting to set resource class", "class-name", selected.GetName())
record.Event(claim, event.Normal(reasonClassFound, "Selected default resource class", "class-name", selected.GetName()))
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Update(ctx, claim), errUpdateClaim)
}

View File

@ -1,271 +0,0 @@
/*
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 claimdefaulting
import (
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &Reconciler{}
func TestReconciler(t *testing.T) {
name := "coolName"
uid := types.UID("definitely-a-uuid")
type args struct {
m manager.Manager
of resource.ClaimKind
to resource.ClassKind
}
type want struct {
result reconcile.Result
err error
}
errBoom := errors.New("boom")
cases := map[string]struct {
args args
want want
}{
"GetClaimError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"ClaimNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{}},
},
"ClaimHasClassRef": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassReferencer: fake.ClassReferencer{Ref: &corev1.ObjectReference{}}}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ListClassesError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errListClasses)},
},
"NoClassesAnnotatedDefault": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(fake.GVK(&fake.Class{}))
u.SetName(name)
u.SetUID(uid)
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"UpdateClaimError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(fake.GVK(&fake.Class{}))
u.SetName(name)
u.SetUID(uid)
u.SetAnnotations(map[string]string{v1alpha1.AnnotationDefaultClassKey: v1alpha1.AnnotationDefaultClassValue})
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errUpdateClaim)},
},
"Successful": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(fake.GVK(&fake.Class{}))
u.SetName(name)
u.SetUID(uid)
u.SetAnnotations(map[string]string{v1alpha1.AnnotationDefaultClassKey: v1alpha1.AnnotationDefaultClassValue})
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetClassReference(&corev1.ObjectReference{
APIVersion: fake.GVK(&fake.Class{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Class{}).Kind,
Name: name,
UID: uid,
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, tc.args.of, tc.args.to, WithDefaultingJitterer(func() {}))
got, err := r.Reconcile(reconcile.Request{})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.result, got); diff != "" {
t.Errorf("r.Reconcile(...): -want, +got:\n%s", diff)
}
})
}
}
func TestReconcilerRandomness(t *testing.T) {
classes := 10
reconciles := 100
refs := make([]*corev1.ObjectReference, 0)
newClass := func(i int) unstructured.Unstructured {
u := &unstructured.Unstructured{}
u.SetUID(types.UID(strconv.Itoa(i)))
u.SetAnnotations(map[string]string{v1alpha1.AnnotationDefaultClassKey: v1alpha1.AnnotationDefaultClassValue})
return *u
}
m := &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
l := o.(*unstructured.UnstructuredList)
for i := 0; i < classes; i++ {
l.Items = append(l.Items, newClass(i))
}
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
ls := obj.(resource.ClassReferencer)
refs = append(refs, ls.GetClassReference())
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
}
r := NewReconciler(m,
resource.ClaimKind(fake.GVK(&fake.Claim{})),
resource.ClassKind(fake.GVK(&fake.Class{})),
WithDefaultingJitterer(func() {}))
for i := 0; i < reconciles; i++ {
r.Reconcile(reconcile.Request{})
}
distribution := map[types.UID]int{}
for _, ref := range refs {
distribution[ref.UID]++
}
// The goal here is to test whether we're random-ish, i.e. that we're not
// picking the same class every time.
if len(distribution) < 2 {
t.Errorf("want > 1 resource classes selected, got %d", len(distribution))
}
}

View File

@ -1,20 +0,0 @@
/*
Copyright 2020 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 claimscheduling provides a reconciler that selects a resource class
// for a resource claim.
// Deprecated: See https://github.com/crossplane/crossplane/issues/1670
package claimscheduling

View File

@ -1,213 +0,0 @@
/*
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 claimscheduling
import (
"context"
"math/rand"
"strings"
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"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/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
claimSchedulingReconcileTimeout = 1 * time.Minute
claimSchedulingReconcileMaxJitterMs = 1500
aShortWait = 30 * time.Second
)
// Error strings.
const (
errGetClaim = "cannot get resource claim"
errUpdateClaim = "cannot update resource claim"
errListClasses = "cannot list resource classes"
)
// Event reasons.
const reasonClassFound = "SelectedResourceClass"
// ControllerName returns the recommended name for controllers that use this
// package to reconcile a particular kind of resource claim.
func ControllerName(kind string) string {
return "claimscheduling/" + strings.ToLower(kind)
}
// A Jitterer sleeps for a random amount of time in order to decrease the chance
// of any one controller predictably winning the race to schedule claims to a
// class, for example because it has fewer classes to list and select from than
// its competitors.
type Jitterer func()
// A Reconciler schedules resource claims to a resource class that matches their
// class selector. Claims are reconciled by randomly selecting a matching
// resource class and attempting to set it as the claim's class reference. The
// Reconciler is designed for use in claim scheduling controllers that race
// several others to schedule a claim.
type Reconciler struct {
client client.Client
newClaim func() resource.Claim
classKind resource.ClassKind
jitter Jitterer
log logging.Logger
record event.Recorder
}
// A ReconcilerOption configures a Reconciler.
type ReconcilerOption func(*Reconciler)
// WithSchedulingJitterer specifies the Jitterer a Reconciler should use.
func WithSchedulingJitterer(j Jitterer) ReconcilerOption {
return func(r *Reconciler) {
r.jitter = j
}
}
// 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
}
}
// NewReconciler returns a Reconciler that schedules resource claims to a
// resource class that matches their class selector.
func NewReconciler(m manager.Manager, of resource.ClaimKind, to resource.ClassKind, o ...ReconcilerOption) *Reconciler {
nc := func() resource.Claim {
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Claim)
}
// Panic early if we've been asked to reconcile a claim or resource kind
// that has not been registered with our controller manager's scheme.
_ = nc()
r := &Reconciler{
client: m.GetClient(),
newClaim: nc,
classKind: to,
jitter: func() {
random := rand.New(rand.NewSource(time.Now().UnixNano()))
time.Sleep(time.Duration(random.Intn(claimSchedulingReconcileMaxJitterMs)) * time.Millisecond)
},
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a resource claim by using its class selector to select and allocate
// it a resource class.
func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log := r.log.WithValues("request", req)
log.Debug("Reconciling")
ctx, cancel := context.WithTimeout(context.Background(), claimSchedulingReconcileTimeout)
defer cancel()
claim := r.newClaim()
if err := r.client.Get(ctx, req.NamespacedName, claim); 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 resource claim", "error", err)
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetClaim)
}
record := r.record.WithAnnotations(
"external-name", meta.GetExternalName(claim),
"class-kind", r.classKind.Kind,
)
log = log.WithValues(
"uid", claim.GetUID(),
"version", claim.GetResourceVersion(),
"external-name", meta.GetExternalName(claim),
"class-kind", r.classKind.Kind,
)
// There could be several controllers racing to schedule this claim. If it
// was scheduled since we were queued then another controller won and we
// should abort.
if claim.GetClassReference() != nil {
log.Debug("Resource class is already set")
return reconcile.Result{Requeue: false}, nil
}
classes := &unstructured.UnstructuredList{}
classes.SetGroupVersionKind(r.classKind.List())
if err := r.client.List(ctx, classes, client.MatchingLabels(claim.GetClassSelector().MatchLabels)); err != nil {
// Claim scheduler controllers don't update the synced status because
// no one scheduler has the full view of whether the process failed or
// succeeded. It's possible another controller can successfully set a
// class even though we can't, so it would be confusing to mark this
// claim as failing to be reconciled. Instead we return an error - we'll
// be requeued but abort immediately if the claim was scheduled.
log.Debug("Cannot list resource classes", "error", err)
return reconcile.Result{}, errors.Wrap(err, errListClasses)
}
if len(classes.Items) == 0 {
// None of our classes matched the selector. We can't be sure whether
// another controller owns classes that matched the selector, or whether
// no classes match, so we requeue after a short wait. We'll abort the
// next reconcile immediately if another controller scheduled the claim.
log.Debug("No matching resource classes found", "requeue-after", time.Now().Add(aShortWait))
return reconcile.Result{RequeueAfter: aShortWait}, nil
}
random := rand.New(rand.NewSource(time.Now().UnixNano()))
selected := classes.Items[random.Intn(len(classes.Items))]
claim.SetClassReference(meta.ReferenceTo(&selected, schema.GroupVersionKind(r.classKind)))
// There could be several controllers racing to schedule this claim to a
// class. We sleep for a randomly jittered amount of time before trying to
// update the class reference to decrease the chance of any one controller
// predictably winning the race, for example because it has fewer classes to
// list and select from than its competitors.
r.jitter()
// Attempt to set the class reference. If a competing controller beat us
// we'll fail the write because the claim's resource version has changed
// since we read it. We'll be requeued, but will abort immediately if the
// claim was scheduled.
log.Debug("Attempting to set resource class", "class-name", selected.GetName())
record.Event(claim, event.Normal(reasonClassFound, "Selected matching resource class", "class-name", selected.GetName()))
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Update(ctx, claim), errUpdateClaim)
}

View File

@ -1,276 +0,0 @@
/*
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 claimscheduling
import (
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"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/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &Reconciler{}
func TestReconciler(t *testing.T) {
name := "coolName"
uid := types.UID("definitely-a-uuid")
type args struct {
m manager.Manager
of resource.ClaimKind
to resource.ClassKind
}
type want struct {
result reconcile.Result
err error
}
errBoom := errors.New("boom")
cases := map[string]struct {
args args
want want
}{
"GetClaimError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"ClaimNotFound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{}},
},
"ClaimHasClassRef": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassReferencer: fake.ClassReferencer{Ref: &corev1.ObjectReference{}}}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ListClassesError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errListClasses)},
},
"NoClassesMatchLabels": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"UpdateClaimError": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(fake.GVK(&fake.Class{}))
u.SetName(name)
u.SetUID(uid)
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{err: errors.Wrap(errBoom, errUpdateClaim)},
},
"Successful": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(fake.GVK(&fake.Class{}))
u.SetName(name)
u.SetUID(uid)
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Claim{}
want.SetClassSelector(&metav1.LabelSelector{})
want.SetClassReference(&corev1.ObjectReference{
APIVersion: fake.GVK(&fake.Class{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Class{}).Kind,
Name: name,
UID: uid,
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
},
of: resource.ClaimKind(fake.GVK(&fake.Claim{})),
to: resource.ClassKind(fake.GVK(&fake.Class{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, tc.args.of, tc.args.to, WithSchedulingJitterer(func() {}))
got, err := r.Reconcile(reconcile.Request{})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.result, got); diff != "" {
t.Errorf("r.Reconcile(...): -want, +got:\n%s", diff)
}
})
}
}
func TestReconcilerRandomness(t *testing.T) {
classes := 10
reconciles := 100
refs := make([]*corev1.ObjectReference, 0)
newClass := func(i int) unstructured.Unstructured {
u := &unstructured.Unstructured{}
u.SetUID(types.UID(strconv.Itoa(i)))
return *u
}
m := &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*fake.Claim)
*c = fake.Claim{ClassSelector: fake.ClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
l := o.(*unstructured.UnstructuredList)
for i := 0; i < classes; i++ {
l.Items = append(l.Items, newClass(i))
}
return nil
}),
MockUpdate: test.NewMockUpdateFn(nil, func(obj runtime.Object) error {
ls := obj.(resource.ClassReferencer)
refs = append(refs, ls.GetClassReference())
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Claim{}),
}
r := NewReconciler(m,
resource.ClaimKind(fake.GVK(&fake.Claim{})),
resource.ClassKind(fake.GVK(&fake.Class{})),
WithSchedulingJitterer(func() {}))
for i := 0; i < reconciles; i++ {
r.Reconcile(reconcile.Request{})
}
distribution := map[types.UID]int{}
for _, ref := range refs {
distribution[ref.UID]++
}
// The goal here is to test whether we're random-ish, i.e. that we're not
// picking the same class every time.
if len(distribution) < 2 {
t.Errorf("want > 1 resource classes selected, got %d", len(distribution))
}
}

View File

@ -545,7 +545,7 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error)
if meta.WasDeleted(managed) {
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
if observation.ResourceExists && shouldDelete(managed) {
if observation.ResourceExists && managed.GetDeletionPolicy() != v1alpha1.DeletionOrphan {
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
@ -695,23 +695,3 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error)
managed.SetConditions(v1alpha1.ReconcileSuccess())
return reconcile.Result{RequeueAfter: r.longWait}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
}
// TODO(negz): ReclaimPolicy will be deprecated alongside resource claims; this
// should be inlined when claims (and thus reclaim policies) are removed.
func shouldDelete(mg resource.Managed) bool {
switch {
// The deletion policy should take precedence over the reclaim policy.
case mg.GetDeletionPolicy() == v1alpha1.DeletionOrphan:
return false
case mg.GetDeletionPolicy() == v1alpha1.DeletionDelete:
return true
case mg.GetReclaimPolicy() == v1alpha1.ReclaimRetain:
return false
case mg.GetReclaimPolicy() == v1alpha1.ReclaimDelete:
return true
}
// If no policy is set, we default to deleting the resource.
return true
}

View File

@ -207,13 +207,13 @@ func TestReconciler(t *testing.T) {
MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
mg := obj.(*fake.Managed)
mg.SetDeletionTimestamp(&now)
mg.SetReclaimPolicy(v1alpha1.ReclaimDelete)
mg.SetDeletionPolicy(v1alpha1.DeletionDelete)
return nil
}),
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
want := &fake.Managed{}
want.SetDeletionTimestamp(&now)
want.SetReclaimPolicy(v1alpha1.ReclaimDelete)
want.SetDeletionPolicy(v1alpha1.DeletionDelete)
want.SetConditions(v1alpha1.ReconcileError(errors.Wrap(errBoom, errReconcileDelete)))
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "An error deleting an external resource should be reported as a conditioned status."
@ -251,13 +251,13 @@ func TestReconciler(t *testing.T) {
MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
mg := obj.(*fake.Managed)
mg.SetDeletionTimestamp(&now)
mg.SetReclaimPolicy(v1alpha1.ReclaimDelete)
mg.SetDeletionPolicy(v1alpha1.DeletionDelete)
return nil
}),
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj runtime.Object, _ ...client.UpdateOption) error {
want := &fake.Managed{}
want.SetDeletionTimestamp(&now)
want.SetReclaimPolicy(v1alpha1.ReclaimDelete)
want.SetDeletionPolicy(v1alpha1.DeletionDelete)
want.SetConditions(v1alpha1.ReconcileSuccess())
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
reason := "A deleted external resource should be reported as a conditioned status."
@ -759,58 +759,3 @@ func TestReconciler(t *testing.T) {
})
}
}
func TestShouldDelete(t *testing.T) {
cases := map[string]struct {
reason string
mg resource.Managed
want bool
}{
"DeletionPolicyDelete": {
reason: "The delete deletion policy should take precedence over the reclaim policy.",
mg: &fake.Managed{
Orphanable: fake.Orphanable{Policy: v1alpha1.DeletionDelete},
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
},
want: true,
},
"DeletionPolicyOrphan": {
reason: "The orphan deletion policy should take precedence over the reclaim policy.",
mg: &fake.Managed{
Orphanable: fake.Orphanable{Policy: v1alpha1.DeletionOrphan},
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
},
want: false,
},
"ReclaimPolicyDelete": {
reason: "The delete reclaim policy should take effect when no deletion policy exists.",
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimDelete},
},
want: true,
},
"ReclaimPolicyRetain": {
reason: "The retain reclaim policy should take effect when no deletion policy exists.",
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{Policy: v1alpha1.ReclaimRetain},
},
want: false,
},
"NoPolicy": {
reason: "Resources should be deleted when no deletion or reclaim policy is specified.",
mg: &fake.Managed{
Reclaimer: fake.Reclaimer{},
},
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := shouldDelete(tc.mg)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("\nReason: %s\nshouldDelete(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -55,14 +55,14 @@ func TestReconciler(t *testing.T) {
Name: "coolmanagedsecret",
}},
}
cm := &fake.Claim{
ObjectMeta: metav1.ObjectMeta{Namespace: "coolns", Name: "coolclaim"},
tg := &fake.Target{
ObjectMeta: metav1.ObjectMeta{Namespace: "coolns", Name: "cooltarget"},
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{Ref: &v1alpha1.LocalSecretReference{
Name: "coolclaimsecret",
Name: "cooltargetsecret",
}},
}
from := resource.ConnectionSecretFor(mg, fake.GVK(mg))
to := resource.LocalConnectionSecretFor(cm, fake.GVK(cm))
to := resource.LocalConnectionSecretFor(tg, fake.GVK(tg))
fromData := map[string][]byte{"cool": {1}}

View File

@ -43,15 +43,13 @@ const (
// Error strings
const (
errGetTarget = "unable to get Target"
errManagedResourceIsNotBound = "managed resource in Target clusterRef is unbound"
errUpdateTarget = "unable to update Target"
errGetTarget = "unable to get Target"
errUpdateTarget = "unable to update Target"
)
// Event reasons.
const (
reasonSetSecretRef event.Reason = "SetSecretRef"
reasonWaitingUntilBound event.Reason = "WaitingUntilBound"
reasonCannotGetManaged event.Reason = "CannotGetManaged"
reasonCannotPropagateSecret event.Reason = "CannotPropagateSecret"
reasonPropagatedSecret event.Reason = "PropagatedSecret"
@ -201,14 +199,6 @@ func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error)
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
}
if !resource.IsBound(managed) {
// TODO(negz): Should we really consider this an error?
log.Debug("Managed resource is not yet bound to a resource claim", "requeue-after", time.Now().Add(aShortWait))
record.Event(target, event.Normal(reasonWaitingUntilBound, "Managed resource is not yet bound to a resource claim"))
target.SetConditions(SecretPropagationError(errors.New(errManagedResourceIsNotBound)))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
}
if err := r.propagator.PropagateConnection(ctx, target, managed); err != nil {
// If we fail to propagate the connection secret of a bound managed
// resource, we try again after a short wait.

View File

@ -248,67 +248,6 @@ func TestReconciler(t *testing.T) {
result: reconcile.Result{RequeueAfter: aShortWait},
},
},
"ErrorManagedNotBound": {
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
switch o := o.(type) {
case *fake.Target:
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
UID: tguid,
Name: tgname,
Namespace: ns,
}}
tg.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
Name: tgcsname,
})
*o = *tg
return nil
case *fake.Managed:
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
UID: mguid,
Name: mgname,
}}
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
*o = *mg
return nil
default:
return errUnexpected
}
},
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &fake.Target{}
want.SetName(tgname)
want.SetNamespace(ns)
want.SetUID(tguid)
want.SetResourceReference(&corev1.ObjectReference{
Name: mgname,
})
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
want.SetConditions(SecretPropagationError(errors.New(errManagedResourceIsNotBound)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
},
of: resource.TargetKind(fake.GVK(&fake.Target{})),
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
},
want: want{
result: reconcile.Result{RequeueAfter: aShortWait},
},
},
"ErrorSecretPropagationFailed": {
args: args{
m: &fake.Manager{
@ -338,7 +277,6 @@ func TestReconciler(t *testing.T) {
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
*o = *mg
return nil
default:
@ -406,7 +344,6 @@ func TestReconciler(t *testing.T) {
Name: mgcsname,
Namespace: mgcsnamespace,
})
mg.SetBindingPhase(v1alpha1.BindingPhaseBound)
*o = *mg
return nil
default:

View File

@ -56,7 +56,7 @@ func TestPropagateConnection(t *testing.T) {
},
}
cm := &fake.Claim{
cm := &fake.CompositeClaim{
ObjectMeta: metav1.ObjectMeta{Namespace: cmcsns},
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
@ -83,7 +83,7 @@ func TestPropagateConnection(t *testing.T) {
"ClaimDoesNotWantConnectionSecret": {
reason: "The managed resource's secret should not be propagated if the claim does not want to write one",
args: args{
o: &fake.Claim{},
o: &fake.CompositeClaim{},
mg: mg,
},
want: nil,

View File

@ -30,41 +30,6 @@ type adder interface {
Add(item interface{})
}
// EnqueueRequestForClaim enqueues a reconcile.Request for the NamespacedName
// of a ClaimReferencer's ClaimReference.
type EnqueueRequestForClaim struct{}
// Create adds a NamespacedName for the supplied CreateEvent if its Object is a
// ClaimReferencer.
func (e *EnqueueRequestForClaim) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
addClaim(evt.Object, q)
}
// Update adds a NamespacedName for the supplied UpdateEvent if its Objects are
// ClaimReferencers.
func (e *EnqueueRequestForClaim) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
addClaim(evt.ObjectOld, q)
addClaim(evt.ObjectNew, q)
}
// Delete adds a NamespacedName for the supplied DeleteEvent if its Object is a
// ClaimReferencer.
func (e *EnqueueRequestForClaim) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
addClaim(evt.Object, q)
}
// Generic adds a NamespacedName for the supplied GenericEvent if its Object is a
// ClaimReferencer.
func (e *EnqueueRequestForClaim) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
addClaim(evt.Object, q)
}
func addClaim(obj runtime.Object, queue adder) {
if cr, ok := obj.(ClaimReferencer); ok && cr.GetClaimReference() != nil {
queue.Add(reconcile.Request{NamespacedName: meta.NamespacedNameOf(cr.GetClaimReference())})
}
}
// EnqueueRequestForPropagated enqueues a reconcile.Request for the
// NamespacedName of a propagated object, i.e. an object with propagation
// metadata annotations.

View File

@ -20,7 +20,6 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
@ -32,7 +31,7 @@ import (
)
var (
_ handler.EventHandler = &EnqueueRequestForClaim{}
_ handler.EventHandler = &EnqueueRequestForPropagated{}
)
type addFn func(item interface{})
@ -41,40 +40,6 @@ func (fn addFn) Add(item interface{}) {
fn(item)
}
func TestAddClaim(t *testing.T) {
ns := "coolns"
name := "coolname"
cases := map[string]struct {
obj runtime.Object
queue adder
}{
"ObjectIsNotAClaimReferencer": {
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"ObjectHasNilClaimReference": {
obj: &fake.Managed{},
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"ObjectHasClaimReference": {
obj: &fake.Managed{ClaimReferencer: fake.ClaimReferencer{Ref: &corev1.ObjectReference{
Namespace: ns,
Name: name,
}}},
queue: addFn(func(got interface{}) {
want := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: name}}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
}),
},
}
for _, tc := range cases {
addClaim(tc.obj, tc.queue)
}
}
func TestAddPropagated(t *testing.T) {
ns := "coolns"
name := "coolname"
@ -94,11 +59,11 @@ func TestAddPropagated(t *testing.T) {
},
"IsPropagator": {
obj: func() runtime.Object {
cm := &fake.Claim{}
cm.SetNamespace(ns)
cm.SetName(name)
tg := &fake.Target{}
tg.SetNamespace(ns)
tg.SetName(name)
mg := &fake.Managed{}
meta.AllowPropagation(mg, cm)
meta.AllowPropagation(mg, tg)
return mg
}(),
queue: addFn(func(got interface{}) {

View File

@ -33,15 +33,6 @@ import (
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
)
// Bindable is a mock that implements Bindable interface.
type Bindable struct{ Phase v1alpha1.BindingPhase }
// SetBindingPhase sets the BindingPhase.
func (m *Bindable) SetBindingPhase(p v1alpha1.BindingPhase) { m.Phase = p }
// GetBindingPhase sets the BindingPhase.
func (m *Bindable) GetBindingPhase() v1alpha1.BindingPhase { return m.Phase }
// Conditioned is a mock that implements Conditioned interface.
type Conditioned struct{ Conditions []v1alpha1.Condition }
@ -62,24 +53,6 @@ func (m *ClaimReferencer) SetClaimReference(r *corev1.ObjectReference) { m.Ref =
// GetClaimReference gets the ClaimReference.
func (m *ClaimReferencer) GetClaimReference() *corev1.ObjectReference { return m.Ref }
// ClassSelector is a mock that implements ClassSelector interface.
type ClassSelector struct{ Sel *metav1.LabelSelector }
// SetClassSelector sets the ClassSelector.
func (m *ClassSelector) SetClassSelector(s *metav1.LabelSelector) { m.Sel = s }
// GetClassSelector gets the ClassSelector.
func (m *ClassSelector) GetClassSelector() *metav1.LabelSelector { return m.Sel }
// ClassReferencer is a mock that implements ClassReferencer interface.
type ClassReferencer struct{ Ref *corev1.ObjectReference }
// SetClassReference sets the ClassReference.
func (m *ClassReferencer) SetClassReference(r *corev1.ObjectReference) { m.Ref = r }
// GetClassReference gets the ClassReference.
func (m *ClassReferencer) GetClassReference() *corev1.ObjectReference { return m.Ref }
// ManagedResourceReferencer is a mock that implements ManagedResourceReferencer interface.
type ManagedResourceReferencer struct{ Ref *corev1.ObjectReference }
@ -135,15 +108,6 @@ func (m *ConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *v1alph
return m.Ref
}
// Reclaimer is a mock that implements Reclaimer interface.
type Reclaimer struct{ Policy v1alpha1.ReclaimPolicy }
// SetReclaimPolicy sets the ReclaimPolicy.
func (m *Reclaimer) SetReclaimPolicy(p v1alpha1.ReclaimPolicy) { m.Policy = p }
// GetReclaimPolicy gets the ReclaimPolicy.
func (m *Reclaimer) GetReclaimPolicy() v1alpha1.ReclaimPolicy { return m.Policy }
// Orphanable implements the Orphanable interface.
type Orphanable struct{ Policy v1alpha1.DeletionPolicy }
@ -225,67 +189,14 @@ func (o *Object) DeepCopyObject() runtime.Object {
return out
}
// Claim is a mock that implements Claim interface.
type Claim struct {
metav1.ObjectMeta
ClassSelector
ClassReferencer
ManagedResourceReferencer
LocalConnectionSecretWriterTo
v1alpha1.ConditionedStatus
v1alpha1.BindingStatus
}
// GetObjectKind returns schema.ObjectKind.
func (m *Claim) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a copy of the object as runtime.Object
func (m *Claim) DeepCopyObject() runtime.Object {
out := &Claim{}
j, err := json.Marshal(m)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
// Class is a mock that implements Class interface.
type Class struct {
metav1.ObjectMeta
Reclaimer
}
// GetObjectKind returns schema.ObjectKind.
func (m *Class) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a copy of the object as runtime.Object
func (m *Class) DeepCopyObject() runtime.Object {
out := &Class{}
j, err := json.Marshal(m)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
// Managed is a mock that implements Managed interface.
type Managed struct {
metav1.ObjectMeta
ClassReferencer
ClaimReferencer
ProviderReferencer
ProviderConfigReferencer
ConnectionSecretWriterTo
Orphanable
Reclaimer
v1alpha1.ConditionedStatus
v1alpha1.BindingStatus
}
// GetObjectKind returns schema.ObjectKind.
@ -357,7 +268,6 @@ type Composite struct {
CompositionReferencer
ComposedResourcesReferencer
ClaimReferencer
Reclaimer
ConnectionSecretWriterTo
v1alpha1.ConditionedStatus
}

View File

@ -26,13 +26,6 @@ import (
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
)
// A Bindable resource may be bound to another resource. Resources are bindable
// when they available for use.
type Bindable interface {
SetBindingPhase(p v1alpha1.BindingPhase)
GetBindingPhase() v1alpha1.BindingPhase
}
// A Conditioned may have conditions set or retrieved. Conditions are typically
// indicate the status of both a resource and its reconciliation process.
type Conditioned interface {
@ -46,18 +39,6 @@ type ClaimReferencer interface {
GetClaimReference() *corev1.ObjectReference
}
// A ClassSelector may reference a resource class.
type ClassSelector interface {
SetClassSelector(s *metav1.LabelSelector)
GetClassSelector() *metav1.LabelSelector
}
// A ClassReferencer may reference a resource class.
type ClassReferencer interface {
SetClassReference(r *corev1.ObjectReference)
GetClassReference() *corev1.ObjectReference
}
// A ManagedResourceReferencer may reference a concrete managed resource.
type ManagedResourceReferencer interface {
SetResourceReference(r *corev1.ObjectReference)
@ -78,12 +59,6 @@ type ConnectionSecretWriterTo interface {
GetWriteConnectionSecretToReference() *v1alpha1.SecretReference
}
// A Reclaimer may specify a ReclaimPolicy.
type Reclaimer interface {
SetReclaimPolicy(p v1alpha1.ReclaimPolicy)
GetReclaimPolicy() v1alpha1.ReclaimPolicy
}
// An Orphanable resource may specify a DeletionPolicy.
type Orphanable interface {
SetDeletionPolicy(p v1alpha1.DeletionPolicy)
@ -145,60 +120,17 @@ type Object interface {
runtime.Object
}
// A Claim is a Kubernetes object representing an abstract resource claim (e.g.
// an SQL database) that may be bound to a concrete managed resource (e.g. a
// CloudSQL instance).
type Claim interface {
Object
ClassSelector
ClassReferencer
ManagedResourceReferencer
LocalConnectionSecretWriterTo
Conditioned
Bindable
}
// A ClaimList is a list of resource claims.
type ClaimList interface {
runtime.Object
// GetItems returns the list of resource claims.
GetItems() []Claim
}
// A Class is a Kubernetes object representing configuration specifications for
// a managed resource.
type Class interface {
Object
Reclaimer
}
// A ClassList is a list of resource classes.
type ClassList interface {
runtime.Object
// GetItems returns the list of resource classes.
GetItems() []Class
}
// A Managed is a Kubernetes object representing a concrete managed
// resource (e.g. a CloudSQL instance).
type Managed interface {
Object
ClassReferencer
ClaimReferencer
ProviderReferencer
ProviderConfigReferencer
ConnectionSecretWriterTo
Orphanable
Reclaimer
Conditioned
Bindable
}
// A ManagedList is a list of managed resources.

View File

@ -81,23 +81,6 @@ func HasManagedResourceReferenceKind(k ManagedKind) PredicateFn {
}
}
// HasClassReferenceKind accepts objects that reference the supplied resource
// class kind.
func HasClassReferenceKind(k ClassKind) PredicateFn {
return func(obj runtime.Object) bool {
r, ok := obj.(ClassReferencer)
if !ok {
return false
}
if r.GetClassReference() == nil {
return false
}
return r.GetClassReference().GroupVersionKind() == schema.GroupVersionKind(k)
}
}
// IsManagedKind accepts objects that are of the supplied managed resource kind.
func IsManagedKind(k ManagedKind, ot runtime.ObjectTyper) PredicateFn {
return func(obj runtime.Object) bool {
@ -152,51 +135,3 @@ func IsPropagated() PredicateFn {
return nn.Namespace != "" && nn.Name != ""
}
}
// HasClassSelector accepts resource claims that do not specify a resource
// class selector.
func HasClassSelector() PredicateFn {
return func(obj runtime.Object) bool {
cs, ok := obj.(ClassSelector)
if !ok {
return false
}
return cs.GetClassSelector() != nil
}
}
// HasNoClassSelector accepts resource claims that do not specify a resource
// class selector.
func HasNoClassSelector() PredicateFn {
return func(obj runtime.Object) bool {
cs, ok := obj.(ClassSelector)
if !ok {
return false
}
return cs.GetClassSelector() == nil
}
}
// HasNoClassReference accepts resource claims that do not reference a specific
// resource class.
func HasNoClassReference() PredicateFn {
return func(obj runtime.Object) bool {
cr, ok := obj.(ClassReferencer)
if !ok {
return false
}
return cr.GetClassReference() == nil
}
}
// HasNoManagedResourceReference accepts resource claims that do not reference a
// specific managed resource.
func HasNoManagedResourceReference() PredicateFn {
return func(obj runtime.Object) bool {
cr, ok := obj.(ManagedResourceReferencer)
if !ok {
return false
}
return cr.GetResourceReference() == nil
}
}

View File

@ -25,11 +25,9 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
func TestAnyOf(t *testing.T) {
@ -102,88 +100,6 @@ func TestAllOf(t *testing.T) {
}
}
func TestHasManagedResourceReferenceKind(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
c client.Client
kind ManagedKind
want bool
}{
"NotAClassReferencer": {
c: &test.MockClient{},
kind: ManagedKind(fake.GVK(&fake.Managed{})),
want: false,
},
"HasNoResourceReference": {
obj: &fake.Claim{},
kind: ManagedKind(fake.GVK(&fake.Managed{})),
want: false,
},
"HasCorrectResourceReference": {
obj: &fake.Claim{
ManagedResourceReferencer: fake.ManagedResourceReferencer{
Ref: &corev1.ObjectReference{
APIVersion: fake.GVK(&fake.Managed{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Managed{}).Kind,
},
},
},
kind: ManagedKind(fake.GVK(&fake.Managed{})),
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasManagedResourceReferenceKind(tc.kind)(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasManagedResourceReferenceKind(...): -want, +got:\n%s", diff)
}
})
}
}
func TestHasClassReferenceKind(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
c client.Client
kind ClassKind
want bool
}{
"NotAClassReferencer": {
c: &test.MockClient{},
kind: ClassKind(fake.GVK(&fake.Class{})),
want: false,
},
"HasNoClassReference": {
obj: &fake.Claim{},
kind: ClassKind(fake.GVK(&fake.Class{})),
want: false,
},
"HasCorrectClassReference": {
obj: &fake.Claim{
ClassReferencer: fake.ClassReferencer{
Ref: &corev1.ObjectReference{
APIVersion: fake.GVK(&fake.Class{}).GroupVersion().String(),
Kind: fake.GVK(&fake.Class{}).Kind,
},
},
},
kind: ClassKind(fake.GVK(&fake.Class{})),
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasClassReferenceKind(tc.kind)(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasClassReferenceKind(...): -want, +got:\n%s", diff)
}
})
}
}
func TestIsManagedKind(t *testing.T) {
cases := map[string]struct {
kind ManagedKind
@ -198,7 +114,7 @@ func TestIsManagedKind(t *testing.T) {
},
"IsNotKind": {
kind: ManagedKind(fake.GVK(&fake.Managed{})),
ot: MockTyper{GVKs: []schema.GroupVersionKind{fake.GVK(&fake.Claim{})}},
ot: MockTyper{GVKs: []schema.GroupVersionKind{fake.GVK(&fake.Object{})}},
want: false,
},
"ErrorDeterminingKind": {
@ -292,11 +208,11 @@ func TestIsPropagator(t *testing.T) {
},
"IsPropagator": {
obj: func() runtime.Object {
cm := &fake.Claim{}
cm.SetNamespace("somenamespace")
cm.SetName("somename")
o := &fake.Object{}
o.SetNamespace("somenamespace")
o.SetName("somename")
mg := &fake.Managed{}
meta.AllowPropagation(mg, cm)
meta.AllowPropagation(mg, o)
return mg
}(),
want: true,
@ -329,12 +245,12 @@ func TestIsPropagated(t *testing.T) {
},
"IsPropagated": {
obj: func() runtime.Object {
cm := &fake.Claim{}
o := &fake.Object{}
mg := &fake.Managed{}
mg.SetNamespace("somenamespace")
mg.SetName("somename")
meta.AllowPropagation(mg, cm)
return cm
meta.AllowPropagation(mg, o)
return o
}(),
want: true,
},
@ -349,115 +265,3 @@ func TestIsPropagated(t *testing.T) {
})
}
}
func TestHasClassSelector(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
want bool
}{
"NotAClassSelector": {
want: false,
},
"NoClassSelector": {
obj: &fake.Claim{},
want: false,
},
"HasClassSelector": {
obj: &fake.Claim{ClassSelector: fake.ClassSelector{Sel: &v1.LabelSelector{}}},
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasClassSelector()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasClassSelector(...): -want, +got:\n%s", diff)
}
})
}
}
func TestHasNoClassSelector(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
want bool
}{
"NotAClassSelector": {
want: false,
},
"NoClassSelector": {
obj: &fake.Claim{},
want: true,
},
"HasClassSelector": {
obj: &fake.Claim{ClassSelector: fake.ClassSelector{Sel: &v1.LabelSelector{}}},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasNoClassSelector()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasNoClassSelector(...): -want, +got:\n%s", diff)
}
})
}
}
func TestHasNoClassReference(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
want bool
}{
"NotAClassReferencer": {
want: false,
},
"NoClassReference": {
obj: &fake.Claim{},
want: true,
},
"HasClassReference": {
obj: &fake.Claim{ClassReferencer: fake.ClassReferencer{Ref: &corev1.ObjectReference{}}},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasNoClassReference()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasNoClassReference(...): -want, +got:\n%s", diff)
}
})
}
}
func TestHasNoMangedResourceReference(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
want bool
}{
"NotAManagedResourceReferencer": {
want: false,
},
"NoManagedResourceReference": {
obj: &fake.Claim{},
want: true,
},
"HasClassReference": {
obj: &fake.Claim{ManagedResourceReferencer: fake.ManagedResourceReferencer{Ref: &corev1.ObjectReference{}}},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasNoManagedResourceReference()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasNoManagedResourecReference(...): -want, +got:\n%s", diff)
}
})
}
}

View File

@ -41,25 +41,9 @@ const SecretTypeConnection corev1.SecretType = "connection.crossplane.io/v1alpha
const (
ExternalResourceTagKeyKind = "crossplane-kind"
ExternalResourceTagKeyName = "crossplane-name"
ExternalResourceTagKeyClass = "crossplane-class"
ExternalResourceTagKeyProvider = "crossplane-providerconfig"
)
// A ClaimKind contains the type metadata for a kind of resource claim.
type ClaimKind schema.GroupVersionKind
// A ClassKind contains the type metadata for a kind of resource class.
type ClassKind schema.GroupVersionKind
// List returns the list kind associated with a ClassKind.
func (k ClassKind) List() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: k.Group,
Version: k.Version,
Kind: k.Kind + "List",
}
}
// A ManagedKind contains the type metadata for a kind of managed resource.
type ManagedKind schema.GroupVersionKind
@ -94,7 +78,7 @@ type ConnectionPropagatorFn func(ctx context.Context, to LocalConnectionSecretOw
// A ManagedConnectionPropagator is responsible for propagating information
// required to connect to a managed resource (for example the connection secret)
// from the managed resource to its resource claim.
// from the managed resource to a target.
type ManagedConnectionPropagator interface {
PropagateConnection(ctx context.Context, o LocalConnectionSecretOwner, mg Managed) error
}
@ -204,46 +188,6 @@ func IgnoreNotFound(err error) error {
return Ignore(kerrors.IsNotFound, err)
}
// ResolveClassClaimValues validates the supplied claim value against the
// supplied resource class value. If both are non-zero they must match.
func ResolveClassClaimValues(classValue, claimValue string) (string, error) {
if classValue == "" {
return claimValue, nil
}
if claimValue == "" {
return classValue, nil
}
if classValue != claimValue {
return "", errors.Errorf("claim value [%s] does not match class value [%s]", claimValue, classValue)
}
return claimValue, nil
}
// SetBindable indicates that the supplied Bindable is ready for binding to
// another Bindable, such as a resource claim or managed resource by setting its
// binding phase to "Unbound". It is a no-op for Bindables in phases "Bound" or
// "Released", because these phases may not transition back to "Unbound".
func SetBindable(b Bindable) {
switch b.GetBindingPhase() {
case v1alpha1.BindingPhaseBound, v1alpha1.BindingPhaseReleased:
return
default:
b.SetBindingPhase(v1alpha1.BindingPhaseUnbound)
}
}
// IsBindable returns true if the supplied Bindable is ready for binding to
// another Bindable, such as a resource claim or managed
func IsBindable(b Bindable) bool {
return b.GetBindingPhase() == v1alpha1.BindingPhaseUnbound
}
// IsBound returns true if the supplied Bindable is bound to another Bindable,
// such as a resource claim or managed
func IsBound(b Bindable) bool {
return b.GetBindingPhase() == v1alpha1.BindingPhaseBound
}
// IsConditionTrue returns if condition status is true
func IsConditionTrue(c v1alpha1.Condition) bool {
return c.Status == corev1.ConditionTrue
@ -373,9 +317,6 @@ func GetExternalTags(mg Managed) map[string]string {
ExternalResourceTagKeyKind: strings.ToLower(mg.GetObjectKind().GroupVersionKind().GroupKind().String()),
ExternalResourceTagKeyName: mg.GetName(),
}
if mg.GetClassReference() != nil {
tags[ExternalResourceTagKeyClass] = mg.GetClassReference().Name
}
switch {
case mg.GetProviderConfigReference() != nil && mg.GetProviderConfigReference().Name != "":

View File

@ -215,7 +215,7 @@ func TestGetKind(t *testing.T) {
"TooManyKinds": {
args: args{
ot: MockTyper{GVKs: []schema.GroupVersionKind{
fake.GVK(&fake.Claim{}),
fake.GVK(&fake.Object{}),
fake.GVK(&fake.Managed{}),
}},
},
@ -248,10 +248,10 @@ func TestMustCreateObject(t *testing.T) {
}{
"KindRegistered": {
args: args{
kind: fake.GVK(&fake.Claim{}),
oc: fake.SchemeWith(&fake.Claim{}),
kind: fake.GVK(&fake.Managed{}),
oc: fake.SchemeWith(&fake.Managed{}),
},
want: &fake.Claim{},
want: &fake.Managed{},
},
}
@ -302,86 +302,6 @@ func TestIgnore(t *testing.T) {
}
}
func TestResolveClassClaimValues(t *testing.T) {
type args struct {
classValue string
claimValue string
}
type want struct {
err error
value string
}
cases := map[string]struct {
args
want
}{
"ClassValueUnset": {
args: args{claimValue: "cool"},
want: want{value: "cool"},
},
"ClaimValueUnset": {
args: args{classValue: "cool"},
want: want{value: "cool"},
},
"IdenticalValues": {
args: args{classValue: "cool", claimValue: "cool"},
want: want{value: "cool"},
},
"ConflictingValues": {
args: args{classValue: "lame", claimValue: "cool"},
want: want{err: errors.New("claim value [cool] does not match class value [lame]")},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := ResolveClassClaimValues(tc.args.classValue, tc.args.claimValue)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("ResolveClassClaimValues(...): -want error, +got error:\n%s", diff)
}
if diff := cmp.Diff(tc.want.value, got); diff != "" {
t.Errorf("ResolveClassClaimValues(...): -want, +got:\n%s", diff)
}
})
}
}
func TestSetBindable(t *testing.T) {
cases := map[string]struct {
b Bindable
want v1alpha1.BindingPhase
}{
"BindableIsUnbindable": {
b: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseUnbindable}},
want: v1alpha1.BindingPhaseUnbound,
},
"BindableIsUnbound": {
b: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseUnbound}},
want: v1alpha1.BindingPhaseUnbound,
},
"BindableIsBound": {
b: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseBound}},
want: v1alpha1.BindingPhaseBound,
},
"BindableIsReleased": {
b: &fake.Claim{BindingStatus: v1alpha1.BindingStatus{Phase: v1alpha1.BindingPhaseReleased}},
want: v1alpha1.BindingPhaseReleased,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
SetBindable(tc.b)
if diff := cmp.Diff(tc.want, tc.b.GetBindingPhase()); diff != "" {
t.Errorf("SetBindable(...): -got, +want:\n%s", diff)
}
})
}
}
func TestIsConditionTrue(t *testing.T) {
cases := map[string]struct {
c v1alpha1.Condition
@ -653,7 +573,6 @@ func TestConnectionSecretMustBeControllableBy(t *testing.T) {
func TestGetExternalTags(t *testing.T) {
provName := "prov"
className := "classy"
cases := map[string]struct {
o Managed
want map[string]string
@ -663,13 +582,11 @@ func TestGetExternalTags(t *testing.T) {
Name: name,
},
ProviderReferencer: fake.ProviderReferencer{Ref: &v1alpha1.Reference{Name: provName}},
ClassReferencer: fake.ClassReferencer{Ref: &corev1.ObjectReference{Name: className}},
},
want: map[string]string{
ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()),
ExternalResourceTagKeyName: name,
ExternalResourceTagKeyProvider: provName,
ExternalResourceTagKeyClass: className,
},
},
"SuccessfulWithProviderConfig": {
@ -677,13 +594,11 @@ func TestGetExternalTags(t *testing.T) {
Name: name,
},
ProviderConfigReferencer: fake.ProviderConfigReferencer{Ref: &v1alpha1.Reference{Name: provName}},
ClassReferencer: fake.ClassReferencer{Ref: &corev1.ObjectReference{Name: className}},
},
want: map[string]string{
ExternalResourceTagKeyKind: strings.ToLower((&fake.Managed{}).GetObjectKind().GroupVersionKind().GroupKind().String()),
ExternalResourceTagKeyName: name,
ExternalResourceTagKeyProvider: provName,
ExternalResourceTagKeyClass: className,
},
},
}