Merge pull request #48 from negz/notsoclassy

Introduce label selector based class scheduling
This commit is contained in:
Nic Cope 2019-10-23 14:56:52 -07:00 committed by GitHub
commit 0f37bea549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1485 additions and 953 deletions

View File

@ -18,6 +18,17 @@ 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"
)
// The annotation used to make a resource class the default.
const (
AnnotationDefaultClassKey = "resourceclass.crossplane.io/is-default-class"
AnnotationDefaultClassValue = "true"
)
const (
@ -39,6 +50,35 @@ const (
ResourceCredentialsTokenKey = "token"
)
// NOTE(negz): The below secret references differ from ObjectReference and
// LocalObjectReference in that they include only the fields Crossplane needs to
// reference a secret, and make those fields required. This reduces ambiguity in
// the API for resource authors.
// A LocalSecretReference is a reference to a secret in the same namespace as
// the referencer.
type LocalSecretReference struct {
// Name of the secret.
Name string `json:"name"`
}
// A SecretReference is a reference to a secret in an arbitrary namespace.
type SecretReference struct {
// Name of the secret.
Name string `json:"name"`
// Namespace of the secret.
Namespace string `json:"namespace"`
}
// A SecretKeySelector is a reference to a secret key in an arbitrary namespace.
type SecretKeySelector struct {
SecretReference `json:",inline"`
// The key to select.
Key string `json:"key"`
}
// A ResourceClaimSpec defines the desired state of a resource claim.
type ResourceClaimSpec struct {
// WriteConnectionSecretToReference specifies the name of a Secret, in the
@ -47,28 +87,27 @@ type ResourceClaimSpec struct {
// include the endpoint, username, and password required to connect to the
// managed resource bound to this resource claim.
// +optional
WriteConnectionSecretToReference corev1.LocalObjectReference `json:"writeConnectionSecretToRef,omitempty"`
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 PortableClassReference specifies the name of a portable resource class,
// in the same namespace as this resource claim, that will be used to
// dynamically provision a managed resource when the resource claim is
// created. The specified class kind must be aligned with the resource
// claim; e.g. a MySQLInstance kind resource claim always refers to a
// MySQLInstanceClass kind portable resource class. Omit the portable class
// reference if you wish to bind to a specific managed resource (known as
// static binding), or to use the default portable class for the resource
// claim's namespace (if any).
// 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
PortableClassReference *corev1.LocalObjectReference `json:"classRef,omitempty"`
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 portable
// resource class; the resource reference will be automatically populated by
// Crossplane.
// 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"`
}
@ -84,13 +123,12 @@ type ResourceClaimStatus struct {
// A ResourceSpec defines the desired state of a managed resource.
type ResourceSpec struct {
// WriteConnectionSecretToReference specifies the name of a Secret, in the
// same namespace as this managed resource, to which any connection details
// for this managed resource should be written. Connection details
// frequently include the endpoint, username, and password required to
// connect to the managed resource.
// WriteConnectionSecretToReference specifies the namespace and name of a
// Secret to which any connection details for this managed resource should
// be written. Connection details frequently include the endpoint, username,
// and password required to connect to the managed resource.
// +optional
WriteConnectionSecretToReference corev1.LocalObjectReference `json:"writeConnectionSecretToRef,omitempty"`
WriteConnectionSecretToReference *SecretReference `json:"writeConnectionSecretToRef,omitempty"`
// ClaimReference specifies the resource claim to which this managed
// resource will be bound. ClaimReference is set automatically during
@ -99,12 +137,12 @@ type ResourceSpec struct {
// +optional
ClaimReference *corev1.ObjectReference `json:"claimRef,omitempty"`
// NonPortableClassReference specifies the non-portable resource class that
// was used to dynamically provision this managed resource, if any.
// Crossplane does not currently support setting this field manually, per
// ClassReference specifies the resource class that was used to dynamically
// provision this managed resource, if any. Crossplane does not currently
// support setting this field manually, per
// https://github.com/crossplaneio/crossplane-runtime/issues/20
// +optional
NonPortableClassReference *corev1.ObjectReference `json:"classRef,omitempty"`
ClassReference *corev1.ObjectReference `json:"classRef,omitempty"`
// ProviderReference specifies the provider that will be used to create,
// observe, update, and delete this managed resource.
@ -126,10 +164,15 @@ type ResourceStatus struct {
BindingStatus `json:",inline"`
}
// NonPortableClassSpecTemplate defines a template that will be used to create
// the specifications of managed resources dynamically provisioned using a
// resource class.
type NonPortableClassSpecTemplate struct {
// 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.
@ -144,25 +187,3 @@ type NonPortableClassSpecTemplate struct {
// +optional
ReclaimPolicy ReclaimPolicy `json:"reclaimPolicy,omitempty"`
}
// PortableClass contains standard fields that all portable classes should include. Class
// should typically be embedded in a specific portable class.
// A PortableClass connects a portable resource claim to a non-portable resource
// class used for dynamic provisioning.
type PortableClass struct {
// NonPortableClassReference is a reference to a resource class kind that is
// not portable across cloud providers, such as an RDSInstanceClass.
NonPortableClassReference *corev1.ObjectReference `json:"classRef,omitempty"`
}
// SetNonPortableClassReference of this Class
func (c *PortableClass) SetNonPortableClassReference(r *corev1.ObjectReference) {
c.NonPortableClassReference = r
}
// GetNonPortableClassReference of this Class
func (c *PortableClass) GetNonPortableClassReference() *corev1.ObjectReference {
return c.NonPortableClassReference
}

View File

@ -21,7 +21,8 @@ limitations under the License.
package v1alpha1
import (
v1 "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -39,6 +40,26 @@ func (in *BindingStatus) DeepCopy() *BindingStatus {
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
if in.ProviderReference != nil {
in, out := &in.ProviderReference, &out.ProviderReference
*out = new(corev1.ObjectReference)
**out = **in
}
}
// 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
@ -78,41 +99,16 @@ func (in *ConditionedStatus) DeepCopy() *ConditionedStatus {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NonPortableClassSpecTemplate) DeepCopyInto(out *NonPortableClassSpecTemplate) {
func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) {
*out = *in
if in.ProviderReference != nil {
in, out := &in.ProviderReference, &out.ProviderReference
*out = new(v1.ObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NonPortableClassSpecTemplate.
func (in *NonPortableClassSpecTemplate) DeepCopy() *NonPortableClassSpecTemplate {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference.
func (in *LocalSecretReference) DeepCopy() *LocalSecretReference {
if in == nil {
return nil
}
out := new(NonPortableClassSpecTemplate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PortableClass) DeepCopyInto(out *PortableClass) {
*out = *in
if in.NonPortableClassReference != nil {
in, out := &in.NonPortableClassReference, &out.NonPortableClassReference
*out = new(v1.ObjectReference)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortableClass.
func (in *PortableClass) DeepCopy() *PortableClass {
if in == nil {
return nil
}
out := new(PortableClass)
out := new(LocalSecretReference)
in.DeepCopyInto(out)
return out
}
@ -120,15 +116,24 @@ func (in *PortableClass) DeepCopy() *PortableClass {
// 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
out.WriteConnectionSecretToReference = in.WriteConnectionSecretToReference
if in.PortableClassReference != nil {
in, out := &in.PortableClassReference, &out.PortableClassReference
*out = new(v1.LocalObjectReference)
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(v1.ObjectReference)
*out = new(corev1.ObjectReference)
**out = **in
}
}
@ -163,20 +168,24 @@ func (in *ResourceClaimStatus) DeepCopy() *ResourceClaimStatus {
// 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
out.WriteConnectionSecretToReference = in.WriteConnectionSecretToReference
if in.ClaimReference != nil {
in, out := &in.ClaimReference, &out.ClaimReference
*out = new(v1.ObjectReference)
if in.WriteConnectionSecretToReference != nil {
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
*out = new(SecretReference)
**out = **in
}
if in.NonPortableClassReference != nil {
in, out := &in.NonPortableClassReference, &out.NonPortableClassReference
*out = new(v1.ObjectReference)
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.ProviderReference != nil {
in, out := &in.ProviderReference, &out.ProviderReference
*out = new(v1.ObjectReference)
*out = new(corev1.ObjectReference)
**out = **in
}
}
@ -207,3 +216,34 @@ func (in *ResourceStatus) DeepCopy() *ResourceStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
*out = *in
out.SecretReference = in.SecretReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
if in == nil {
return nil
}
out := new(SecretKeySelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretReference) DeepCopyInto(out *SecretReference) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference.
func (in *SecretReference) DeepCopy() *SecretReference {
if in == nil {
return nil
}
out := new(SecretReference)
in.DeepCopyInto(out)
return out
}

View File

@ -58,12 +58,12 @@ func NewAPIManagedCreator(c client.Client, t runtime.ObjectTyper) *APIManagedCre
}
// Create the supplied resource using the supplied class and claim.
func (a *APIManagedCreator) Create(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error {
func (a *APIManagedCreator) Create(ctx context.Context, cm Claim, cs Class, mg Managed) error {
cmr := meta.ReferenceTo(cm, MustGetKind(cm, a.typer))
csr := meta.ReferenceTo(cs, MustGetKind(cs, a.typer))
mg.SetClaimReference(cmr)
mg.SetNonPortableClassReference(csr)
mg.SetClassReference(csr)
if err := a.client.Create(ctx, mg); err != nil {
return errors.Wrap(err, errCreateManaged)
}
@ -93,11 +93,14 @@ func NewAPIManagedConnectionPropagator(c client.Client, t runtime.ObjectTyper) *
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, cm Claim, mg Managed) error {
// Either this resource does not expose a connection secret, or this claim
// does not want one.
if mg.GetWriteConnectionSecretToReference().Name == "" || cm.GetWriteConnectionSecretToReference().Name == "" {
if mg.GetWriteConnectionSecretToReference() == nil || cm.GetWriteConnectionSecretToReference() == nil {
return nil
}
n := types.NamespacedName{Namespace: mg.GetNamespace(), Name: mg.GetWriteConnectionSecretToReference().Name}
n := types.NamespacedName{
Namespace: mg.GetWriteConnectionSecretToReference().Namespace,
Name: mg.GetWriteConnectionSecretToReference().Name,
}
mgcs := &corev1.Secret{}
if err := a.client.Get(ctx, n, mgcs); err != nil {
return errors.Wrap(err, errGetSecret)
@ -111,7 +114,7 @@ func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context
return errors.New(errSecretConflict)
}
cmcs := ConnectionSecretFor(cm, MustGetKind(cm, a.typer))
cmcs := LocalConnectionSecretFor(cm, MustGetKind(cm, a.typer))
if _, err := util.CreateOrUpdate(ctx, a.client, cmcs, func() error {
// Inside this anonymous function cmcs could either be unchanged (if
// it does not exist in the API server) or updated to reflect its

View File

@ -53,7 +53,7 @@ func TestCreate(t *testing.T) {
type args struct {
ctx context.Context
cm Claim
cs NonPortableClass
cs Class
mg Managed
}
@ -72,12 +72,12 @@ func TestCreate(t *testing.T) {
client: &test.MockClient{
MockCreate: test.NewMockCreateFn(errBoom),
},
typer: MockSchemeWith(&MockClaim{}, &MockNonPortableClass{}, &MockManaged{}),
typer: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
args: args{
ctx: context.Background(),
cm: &MockClaim{},
cs: &MockNonPortableClass{},
cs: &MockClass{},
mg: &MockManaged{},
},
want: errors.Wrap(errBoom, errCreateManaged),
@ -88,12 +88,12 @@ func TestCreate(t *testing.T) {
MockCreate: test.NewMockCreateFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
typer: MockSchemeWith(&MockClaim{}, &MockNonPortableClass{}, &MockManaged{}),
typer: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
args: args{
ctx: context.Background(),
cm: &MockClaim{},
cs: &MockNonPortableClass{},
cs: &MockClass{},
mg: &MockManaged{},
},
want: errors.Wrap(errBoom, errUpdateClaim),
@ -109,10 +109,10 @@ func TestCreate(t *testing.T) {
APIVersion: MockGVK(&MockClaim{}).GroupVersion().String(),
Kind: MockGVK(&MockClaim{}).Kind,
})
want.SetNonPortableClassReference(&corev1.ObjectReference{
want.SetClassReference(&corev1.ObjectReference{
Name: csname,
APIVersion: MockGVK(&MockNonPortableClass{}).GroupVersion().String(),
Kind: MockGVK(&MockNonPortableClass{}).Kind,
APIVersion: MockGVK(&MockClass{}).GroupVersion().String(),
Kind: MockGVK(&MockClass{}).Kind,
})
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
@ -134,12 +134,12 @@ func TestCreate(t *testing.T) {
return nil
}),
},
typer: MockSchemeWith(&MockClaim{}, &MockNonPortableClass{}, &MockManaged{}),
typer: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
args: args{
ctx: context.Background(),
cm: &MockClaim{ObjectMeta: metav1.ObjectMeta{Name: cmname}},
cs: &MockNonPortableClass{ObjectMeta: metav1.ObjectMeta{Name: csname}},
cs: &MockClass{ObjectMeta: metav1.ObjectMeta{Name: csname}},
mg: &MockManaged{ObjectMeta: metav1.ObjectMeta{Name: mgname}},
},
want: nil,
@ -173,6 +173,7 @@ func TestPropagateConnection(t *testing.T) {
uid := types.UID("definitely-a-uuid")
cmcsname := "coolclaimsecret"
mgcsname := "coolmanagedsecret"
mgcsnamespace := "coolns"
mgcsdata := map[string][]byte{"cool": []byte("data")}
controller := true
errBoom := errors.New("boom")
@ -187,7 +188,9 @@ func TestPropagateConnection(t *testing.T) {
ctx: context.Background(),
cm: &MockClaim{},
mg: &MockManaged{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: nil,
@ -196,7 +199,9 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: mgcsname},
},
},
mg: &MockManaged{},
},
@ -209,10 +214,14 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &MockManaged{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: errors.Wrap(errBoom, errGetSecret),
@ -248,12 +257,16 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: errors.Wrap(errors.New(errSecretConflict), errCreateOrUpdateSecret),
@ -285,12 +298,16 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: errors.Wrap(errors.New(errSecretConflict), errCreateOrUpdateSecret),
@ -331,12 +348,16 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
ObjectMeta: metav1.ObjectMeta{Name: cmname},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: errors.Wrap(errBoom, errUpdateSecret),
@ -402,12 +423,16 @@ func TestPropagateConnection(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: cmname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: cmcsname}},
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: cmname, UID: uid},
MockLocalConnectionSecretWriterTo: MockLocalConnectionSecretWriterTo{
Ref: &v1alpha1.LocalSecretReference{Name: cmcsname},
},
},
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname, UID: uid},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{
Ref: &v1alpha1.SecretReference{Namespace: mgcsnamespace, Name: mgcsname},
},
},
},
want: nil,

View File

@ -24,7 +24,6 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@ -57,14 +56,16 @@ var log = logging.Logger.WithName("controller")
// A ClaimKind contains the type metadata for a kind of resource claim.
type ClaimKind schema.GroupVersionKind
// A NonPortableClassKind contains the type metadata for a kind of
// non-portable resource class.
type NonPortableClassKind schema.GroupVersionKind
// A ClassKind contains the type metadata for a kind of resource class.
type ClassKind schema.GroupVersionKind
// A ClassKinds contains the type metadata for a kind of resource class.
type ClassKinds struct {
Portable schema.GroupVersionKind
NonPortable 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.
@ -73,15 +74,15 @@ type ManagedKind schema.GroupVersionKind
// 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 Claim, cs NonPortableClass, mg Managed) error
Configure(ctx context.Context, cm Claim, cs Class, mg Managed) error
}
// A ManagedConfiguratorFn is a function that satisfies the
// ManagedConfigurator interface.
type ManagedConfiguratorFn func(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
type ManagedConfiguratorFn func(ctx context.Context, cm Claim, cs Class, mg Managed) error
// Configure the supplied resource using the supplied claim and class.
func (fn ManagedConfiguratorFn) Configure(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error {
func (fn ManagedConfiguratorFn) Configure(ctx context.Context, cm Claim, cs Class, mg Managed) error {
return fn(ctx, cm, cs, mg)
}
@ -90,14 +91,14 @@ func (fn ManagedConfiguratorFn) Configure(ctx context.Context, cm Claim, cs NonP
// 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 Claim, cs NonPortableClass, mg Managed) error
Create(ctx context.Context, cm Claim, cs Class, mg Managed) error
}
// A ManagedCreatorFn is a function that satisfies the ManagedCreator interface.
type ManagedCreatorFn func(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error
type ManagedCreatorFn func(ctx context.Context, cm Claim, cs Class, mg Managed) error
// Create the supplied resource.
func (fn ManagedCreatorFn) Create(ctx context.Context, cm Claim, cs NonPortableClass, mg Managed) error {
func (fn ManagedCreatorFn) Create(ctx context.Context, cm Claim, cs Class, mg Managed) error {
return fn(ctx, cm, cs, mg)
}
@ -164,11 +165,10 @@ func (fn ClaimFinalizerFn) Finalize(ctx context.Context, cm Claim) error {
// type of resource class provisioner. Each controller must watch its subset of
// resource claims and any managed resources they control.
type ClaimReconciler struct {
client client.Client
newClaim func() Claim
newNonPortableClass func() NonPortableClass
newPortableClass func() PortableClass
newManaged func() Managed
client client.Client
newClaim func() Claim
newClass func() Class
newManaged func() Managed
// The below structs embed the set of interfaces used to implement the
// resource claim reconciler. We do this primarily for readability, so that
@ -203,7 +203,7 @@ func defaultCRClaim(m manager.Manager) crClaim {
return crClaim{ClaimFinalizer: NewAPIClaimFinalizerRemover(m.GetClient())}
}
// A ClaimReconcilerOption configures a Reconciler.
// A ClaimReconcilerOption configures a ClaimReconciler.
type ClaimReconcilerOption func(*ClaimReconciler)
// WithManagedConfigurators specifies which configurators should be used to
@ -261,24 +261,22 @@ func WithClaimFinalizer(f ClaimFinalizer) ClaimReconcilerOption {
// with the supplied manager's runtime.Scheme. The returned ClaimReconciler will
// apply only the ObjectMetaConfigurator by default; most callers should supply
// one or more ManagedConfigurators to configure their managed resources.
func NewClaimReconciler(m manager.Manager, of ClaimKind, using ClassKinds, with ManagedKind, o ...ClaimReconcilerOption) *ClaimReconciler {
func NewClaimReconciler(m manager.Manager, of ClaimKind, using ClassKind, with ManagedKind, o ...ClaimReconcilerOption) *ClaimReconciler {
nc := func() Claim { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(Claim) }
ns := func() NonPortableClass { return MustCreateObject(using.NonPortable, m.GetScheme()).(NonPortableClass) }
np := func() PortableClass { return MustCreateObject(using.Portable, m.GetScheme()).(PortableClass) }
ns := func() Class { return MustCreateObject(schema.GroupVersionKind(using), m.GetScheme()).(Class) }
nr := func() Managed { return MustCreateObject(schema.GroupVersionKind(with), m.GetScheme()).(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(), np(), nr()
_, _, _ = nc(), ns(), nr()
r := &ClaimReconciler{
client: m.GetClient(),
newClaim: nc,
newNonPortableClass: ns,
newPortableClass: np,
newManaged: nr,
managed: defaultCRManaged(m),
claim: defaultCRClaim(m),
client: m.GetClient(),
newClaim: nc,
newClass: ns,
newManaged: nr,
managed: defaultCRManaged(m),
claim: defaultCRClaim(m),
}
for _, ro := range o {
@ -342,26 +340,12 @@ func (r *ClaimReconciler) Reconcile(req reconcile.Request) (reconcile.Result, er
}
if !meta.WasCreated(managed) {
portable := r.newPortableClass()
// Portable class reference should always be set by the time we get this far;
// Class reference should always be set by the time we get this far;
// Our watch predicates require it.
p := types.NamespacedName{
Namespace: claim.GetNamespace(),
Name: claim.GetPortableClassReference().Name,
}
if err := r.client.Get(ctx, p, portable); 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
// portable class is (re)created.
claim.SetConditions(v1alpha1.Creating(), v1alpha1.ReconcileError(err))
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, claim), errUpdateClaimStatus)
}
class := r.newNonPortableClass()
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(portable.GetNonPortableClassReference()), class); err != nil {
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

View File

@ -63,7 +63,7 @@ func TestClaimReconciler(t *testing.T) {
type args struct {
m manager.Manager
of ClaimKind
use ClassKinds
use ClassKind
with ManagedKind
o []ClaimReconcilerOption
}
@ -85,10 +85,10 @@ func TestClaimReconciler(t *testing.T) {
args: args{
m: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
@ -120,10 +120,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
@ -153,10 +153,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedFinalizer(ManagedFinalizerFn(func(_ context.Context, _ Managed) error { return errBoom })),
@ -189,10 +189,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedFinalizer(ManagedFinalizerFn(func(_ context.Context, _ Managed) error { return nil })),
@ -226,10 +226,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedFinalizer(ManagedFinalizerFn(func(_ context.Context, _ Managed) error { return nil })),
@ -263,10 +263,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedFinalizer(ManagedFinalizerFn(func(_ context.Context, _ Managed) error { return nil })),
@ -275,41 +275,6 @@ func TestClaimReconciler(t *testing.T) {
},
want: want{result: reconcile.Result{Requeue: false}},
},
"GetPortableClassError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockClaim:
cm := &MockClaim{}
cm.SetPortableClassReference(&corev1.LocalObjectReference{})
*o = *cm
return nil
case *MockPortableClass:
return errBoom
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetPortableClassReference(&corev1.LocalObjectReference{})
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
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
},
"GetResourceClassError": {
args: args{
m: &MockManager{
@ -318,15 +283,10 @@ func TestClaimReconciler(t *testing.T) {
switch o := o.(type) {
case *MockClaim:
cm := &MockClaim{}
cm.SetPortableClassReference(&corev1.LocalObjectReference{})
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *MockPortableClass:
pc := &MockPortableClass{}
pc.SetNonPortableClassReference(&corev1.ObjectReference{})
*o = *pc
return nil
case *MockNonPortableClass:
case *MockClass:
return errBoom
default:
return errUnexpected
@ -334,7 +294,7 @@ func TestClaimReconciler(t *testing.T) {
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetPortableClassReference(&corev1.LocalObjectReference{})
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)
@ -342,10 +302,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
@ -358,15 +318,10 @@ func TestClaimReconciler(t *testing.T) {
switch o := o.(type) {
case *MockClaim:
cm := &MockClaim{}
cm.SetPortableClassReference(&corev1.LocalObjectReference{})
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *MockPortableClass:
pc := &MockPortableClass{}
pc.SetNonPortableClassReference(&corev1.ObjectReference{})
*o = *pc
return nil
case *MockNonPortableClass:
case *MockClass:
return nil
default:
return errUnexpected
@ -374,7 +329,7 @@ func TestClaimReconciler(t *testing.T) {
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetPortableClassReference(&corev1.LocalObjectReference{})
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)
@ -382,13 +337,13 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{WithManagedConfigurators(ManagedConfiguratorFn(
func(_ context.Context, _ Claim, _ NonPortableClass, _ Managed) error { return errBoom },
func(_ context.Context, _ Claim, _ Class, _ Managed) error { return errBoom },
))},
},
want: want{result: reconcile.Result{RequeueAfter: aShortWait}},
@ -401,15 +356,10 @@ func TestClaimReconciler(t *testing.T) {
switch o := o.(type) {
case *MockClaim:
cm := &MockClaim{}
cm.SetPortableClassReference(&corev1.LocalObjectReference{})
cm.SetClassReference(&corev1.ObjectReference{})
*o = *cm
return nil
case *MockPortableClass:
pc := &MockPortableClass{}
pc.SetNonPortableClassReference(&corev1.ObjectReference{})
*o = *pc
return nil
case *MockNonPortableClass:
case *MockClass:
return nil
default:
return errUnexpected
@ -417,7 +367,7 @@ func TestClaimReconciler(t *testing.T) {
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetPortableClassReference(&corev1.LocalObjectReference{})
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)
@ -425,17 +375,17 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedConfigurators(ManagedConfiguratorFn(
func(_ context.Context, _ Claim, _ NonPortableClass, _ Managed) error { return nil },
func(_ context.Context, _ Claim, _ Class, _ Managed) error { return nil },
)),
WithManagedCreator(ManagedCreatorFn(
func(_ context.Context, _ Claim, _ NonPortableClass, _ Managed) error { return errBoom },
func(_ context.Context, _ Claim, _ Class, _ Managed) error { return errBoom },
)),
},
},
@ -474,10 +424,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{Requeue: false}},
@ -513,10 +463,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{Requeue: false}},
@ -552,10 +502,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedConnectionPropagator(ManagedConnectionPropagatorFn(
@ -596,10 +546,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
o: []ClaimReconcilerOption{
WithManagedConnectionPropagator(ManagedConnectionPropagatorFn(
@ -643,10 +593,10 @@ func TestClaimReconciler(t *testing.T) {
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}, &MockManaged{}),
s: MockSchemeWith(&MockClaim{}, &MockClass{}, &MockManaged{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
use: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
use: ClassKind(MockGVK(&MockClass{})),
with: ManagedKind(MockGVK(&MockManaged{})),
},
want: want{result: reconcile.Result{Requeue: false}},

View File

@ -0,0 +1,154 @@
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"math/rand"
"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/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/logging"
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
const (
claimDefaultingControllerName = "resourceclaimdefaulter.crossplane.io"
claimDefaultingReconcileTimeout = 1 * time.Minute
claimDefaultingReconcileMaxJitterMs = 1500
)
// A ClaimDefaultingReconciler 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 ClaimDefaultingReconciler struct {
client client.Client
newClaim func() Claim
classKind ClassKind
jitter Jitterer
}
// A ClaimDefaultingReconcilerOption configures a ClaimDefaultingReconciler.
type ClaimDefaultingReconcilerOption func(*ClaimDefaultingReconciler)
// WithDefaultingJitterer specifies the Jitterer a ClaimDefaultingReconciler should use.
func WithDefaultingJitterer(j Jitterer) ClaimDefaultingReconcilerOption {
return func(r *ClaimDefaultingReconciler) {
r.jitter = j
}
}
// NewClaimDefaultingReconciler returns a ClaimDefaultingReconciler that sets
// the class reference of a resource claim to the resource class annotated as
// the default.
func NewClaimDefaultingReconciler(m manager.Manager, of ClaimKind, to ClassKind, o ...ClaimDefaultingReconcilerOption) *ClaimDefaultingReconciler {
nc := func() Claim { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(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 := &ClaimDefaultingReconciler{
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)
},
}
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 *ClaimDefaultingReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log.V(logging.Debug).Info("Reconciling", "controller", claimDefaultingControllerName, "request", req)
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.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetClaim)
}
// 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 {
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.
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.
// There's nothing for us to do.
return reconcile.Result{Requeue: false}, 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.
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Update(ctx, claim), errUpdateClaim)
}

View File

@ -0,0 +1,269 @@
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"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/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &ClaimDefaultingReconciler{}
func TestClaimDefaultingReconciler(t *testing.T) {
name := "coolName"
uid := types.UID("definitely-a-uuid")
type args struct {
m manager.Manager
of ClaimKind
to 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: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"ClaimNotFound": {
args: args{
m: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{}},
},
"ClaimHasClassRef": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassReferencer: MockClassReferencer{Ref: &corev1.ObjectReference{}}}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ListClassesError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(errBoom),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errListClasses)},
},
"NoClassesAnnotatedDefault": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(MockGVK(&MockClass{}))
u.SetName(name)
u.SetUID(uid)
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"UpdateClaimError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(MockGVK(&MockClass{}))
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),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errUpdateClaim)},
},
"Successful": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(MockGVK(&MockClass{}))
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 := &MockClaim{}
want.SetClassReference(&corev1.ObjectReference{
APIVersion: MockGVK(&MockClass{}).GroupVersion().String(),
Kind: MockGVK(&MockClass{}).Kind,
Name: name,
UID: uid,
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewClaimDefaultingReconciler(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 TestClaimDefaultingReconcilerRandomness(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 := &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{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.(ClassReferencer)
refs = append(refs, ls.GetClassReference())
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
}
r := NewClaimDefaultingReconciler(m,
ClaimKind(MockGVK(&MockClaim{})),
ClassKind(MockGVK(&MockClass{})),
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

@ -0,0 +1,157 @@
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"math/rand"
"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/crossplaneio/crossplane-runtime/pkg/logging"
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
const (
claimSchedulingControllerName = "resourceclaimscheduler.crossplane.io"
claimSchedulingReconcileTimeout = 1 * time.Minute
claimSchedulingReconcileMaxJitterMs = 1500
)
const (
errListClasses = "cannot list resource classes"
)
// 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 ClaimSchedulingReconciler 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 ClaimSchedulingReconciler is designed for use in
// claim scheduling controllers that race several others to schedule a claim.
type ClaimSchedulingReconciler struct {
client client.Client
newClaim func() Claim
classKind ClassKind
jitter Jitterer
}
// A ClaimSchedulingReconcilerOption configures a ClaimSchedulingReconciler.
type ClaimSchedulingReconcilerOption func(*ClaimSchedulingReconciler)
// WithSchedulingJitterer specifies the Jitterer a ClaimSchedulingReconciler should use.
func WithSchedulingJitterer(j Jitterer) ClaimSchedulingReconcilerOption {
return func(r *ClaimSchedulingReconciler) {
r.jitter = j
}
}
// NewClaimSchedulingReconciler returns a ClaimSchedulingReconciler that
// schedules resource claims to a resource class that matches their class
// selector.
func NewClaimSchedulingReconciler(m manager.Manager, of ClaimKind, to ClassKind, o ...ClaimSchedulingReconcilerOption) *ClaimSchedulingReconciler {
nc := func() Claim { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(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 := &ClaimSchedulingReconciler{
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)
},
}
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 *ClaimSchedulingReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log.V(logging.Debug).Info("Reconciling", "controller", claimSchedulingControllerName, "request", req)
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.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetClaim)
}
// 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 {
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.
return reconcile.Result{}, errors.Wrap(err, errListClasses)
}
if len(classes.Items) == 0 {
// None of our classes matched the class selector.
// There's nothing for us to do.
return reconcile.Result{Requeue: false}, 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.
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Update(ctx, claim), errUpdateClaim)
}

View File

@ -0,0 +1,274 @@
/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"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/crossplaneio/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &ClaimDefaultingReconciler{}
func TestClaimSchedulingReconciler(t *testing.T) {
name := "coolName"
uid := types.UID("definitely-a-uuid")
type args struct {
m manager.Manager
of ClaimKind
to 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: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"ClaimNotFound": {
args: args{
m: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{}},
},
"ClaimHasClassRef": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassReferencer: MockClassReferencer{Ref: &corev1.ObjectReference{}}}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"ListClassesError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(errBoom),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errListClasses)},
},
"NoClassesMatchLabels": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
"UpdateClaimError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(MockGVK(&MockClass{}))
u.SetName(name)
u.SetUID(uid)
l := o.(*unstructured.UnstructuredList)
l.Items = []unstructured.Unstructured{*u}
return nil
}),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{err: errors.Wrap(errBoom, errUpdateClaim)},
},
"Successful": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{Sel: &metav1.LabelSelector{}}}
return nil
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(MockGVK(&MockClass{}))
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 := &MockClaim{}
want.SetClassSelector(&metav1.LabelSelector{})
want.SetClassReference(&corev1.ObjectReference{
APIVersion: MockGVK(&MockClass{}).GroupVersion().String(),
Kind: MockGVK(&MockClass{}).Kind,
Name: name,
UID: uid,
})
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
to: ClassKind(MockGVK(&MockClass{})),
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewClaimSchedulingReconciler(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 TestClaimSchedulingReconcilerRandomness(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 := &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
c := o.(*MockClaim)
*c = MockClaim{MockClassSelector: MockClassSelector{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.(ClassReferencer)
refs = append(refs, ls.GetClassReference())
return nil
}),
},
s: MockSchemeWith(&MockClaim{}),
}
r := NewClaimSchedulingReconciler(m,
ClaimKind(MockGVK(&MockClaim{})),
ClassKind(MockGVK(&MockClass{})),
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

@ -31,7 +31,7 @@ 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 Claim, cs NonPortableClass, mg Managed) error {
func (cc ConfiguratorChain) Configure(ctx context.Context, cm Claim, cs Class, mg Managed) error {
for _, c := range cc {
if err := c.Configure(ctx, cm, cs, mg); err != nil {
return err
@ -52,16 +52,21 @@ func NewObjectMetaConfigurator(t runtime.ObjectTyper) *ObjectMetaConfigurator {
}
// Configure the supplied Managed resource's object metadata.
func (c *ObjectMetaConfigurator) Configure(_ context.Context, cm Claim, cs NonPortableClass, mg Managed) error {
mg.SetNamespace(cs.GetNamespace())
func (c *ObjectMetaConfigurator) Configure(_ context.Context, cm Claim, _ Class, mg Managed) error {
mg.SetGenerateName(fmt.Sprintf("%s-%s-", cm.GetNamespace(), cm.GetName()))
if meta.GetExternalName(cm) != "" {
meta.SetExternalName(mg, meta.GetExternalName(cm))
}
// TODO(negz): Don't set this potentially cross-namespace owner reference.
// We probably want to use the resource's reclaim policy, not Kubernetes
// garbage collection, to determine whether to delete a managed resource
// when its claim is deleted per https://github.com/crossplaneio/crossplane/issues/550
// TODO(negz): Avoid setting this owner reference? Kubernetes specifies that
// cluster scoped resources cannot have namespaced owners, by design, but
// the owner reference appears to work for cascading deletes.
// https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/#owners-and-dependents
// TODO(negz): We probably want to use the resource's reclaim policy, not
// Kubernetes garbage collection, to determine whether to delete a managed
// resource when its claim is deleted per
// https://github.com/crossplaneio/crossplane/issues/550
mg.SetOwnerReferences([]v1.OwnerReference{meta.AsOwner(meta.ReferenceTo(cm, MustGetKind(cm, c.typer)))})
return nil

View File

@ -40,7 +40,7 @@ func TestConfiguratorChain(t *testing.T) {
type args struct {
ctx context.Context
cm Claim
cs NonPortableClass
cs Class
mg Managed
}
@ -54,35 +54,35 @@ func TestConfiguratorChain(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{},
cs: &MockNonPortableClass{},
cs: &MockClass{},
mg: &MockManaged{},
},
want: nil,
},
"SuccessulConfigurator": {
cc: ConfiguratorChain{
ManagedConfiguratorFn(func(_ context.Context, _ Claim, _ NonPortableClass, _ Managed) error {
ManagedConfiguratorFn(func(_ context.Context, _ Claim, _ Class, _ Managed) error {
return nil
}),
},
args: args{
ctx: context.Background(),
cm: &MockClaim{},
cs: &MockNonPortableClass{},
cs: &MockClass{},
mg: &MockManaged{},
},
want: nil,
},
"ConfiguratorReturnsError": {
cc: ConfiguratorChain{
ManagedConfiguratorFn(func(_ context.Context, _ Claim, _ NonPortableClass, _ Managed) error {
ManagedConfiguratorFn(func(_ context.Context, _ Claim, _ Class, _ Managed) error {
return errBoom
}),
},
args: args{
ctx: context.Background(),
cm: &MockClaim{},
cs: &MockNonPortableClass{},
cs: &MockClass{},
mg: &MockManaged{},
},
want: errBoom,
@ -100,7 +100,6 @@ func TestConfiguratorChain(t *testing.T) {
}
func TestConfigureObjectMeta(t *testing.T) {
ns := "namespace"
claimName := "myclaim"
claimNS := "myclaimns"
uid := types.UID("definitely-a-uuid")
@ -108,7 +107,7 @@ func TestConfigureObjectMeta(t *testing.T) {
type args struct {
ctx context.Context
cm Claim
cs NonPortableClass
cs Class
mg Managed
}
@ -127,12 +126,10 @@ func TestConfigureObjectMeta(t *testing.T) {
args: args{
ctx: context.Background(),
cm: &MockClaim{ObjectMeta: metav1.ObjectMeta{Name: claimName, Namespace: claimNS, UID: uid}},
cs: &MockNonPortableClass{ObjectMeta: metav1.ObjectMeta{Namespace: ns}},
mg: &MockManaged{},
},
want: want{
mg: &MockManaged{ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
GenerateName: claimNS + "-" + claimName + "-",
OwnerReferences: []metav1.OwnerReference{{
APIVersion: MockGVK(&MockClaim{}).GroupVersion().String(),

View File

@ -1,170 +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 resource
import (
"context"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"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"
corev1alpha1 "github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/logging"
)
const (
controllerNameDefaultClass = "defaultclass.crossplane.io"
defaultClassWait = 1 * time.Minute
defaultClassReconcileTimeout = 1 * time.Minute
)
// Label values for listing default portable classes
const (
LabelKeyDefaultClass = "default"
LabelValueTrue = "true"
)
// Error strings
const (
errFailedList = "unable to list portable classes for claim kind"
errNoPortableClass = "unable to locate a portable class that specifies a default class for claim kind"
errMultiplePortableClasses = "multiple portable classes that specify a default class defined for claim kind"
)
// A PortableClassKind contains the type metadata for a kind of portable class.
type PortableClassKind struct {
Singular schema.GroupVersionKind
Plural schema.GroupVersionKind
}
// DefaultClassReconciler reconciles resource claims to the
// default resource class for their given kind according to existing
// portable classes. Predicates ensure that only claims with no resource class
// reference are reconciled.
type DefaultClassReconciler struct {
client client.Client
converter runtime.ObjectConvertor
labels map[string]string
newClaim func() Claim
newPortableClass func() PortableClass
newPortableClassList func() PortableClassList
}
// A DefaultClassReconcilerOption configures a DefaultClassReconciler.
type DefaultClassReconcilerOption func(*DefaultClassReconciler)
// WithObjectConverter specifies how the DefaultClassReconciler should convert
// an *UnstructuredList into a concrete list type.
func WithObjectConverter(oc runtime.ObjectConvertor) DefaultClassReconcilerOption {
return func(r *DefaultClassReconciler) {
r.converter = oc
}
}
// WithLabels specifies how the DefaultClassReconciler should search
// for a default class
func WithLabels(labels map[string]string) DefaultClassReconcilerOption {
return func(r *DefaultClassReconciler) {
r.labels = labels
}
}
// NewDefaultClassReconciler creates a new DefaultReconciler for the claim kind.
func NewDefaultClassReconciler(m manager.Manager, of ClaimKind, by PortableClassKind, o ...DefaultClassReconcilerOption) *DefaultClassReconciler {
nc := func() Claim { return MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(Claim) }
np := func() PortableClass { return MustCreateObject(by.Singular, m.GetScheme()).(PortableClass) }
npl := func() PortableClassList { return MustCreateObject(by.Plural, m.GetScheme()).(PortableClassList) }
// Panic early if we've been asked to reconcile a claim, portable class, or portable class list that has
// not been registered with our controller manager's scheme.
_, _, _ = nc(), np(), npl()
labels := map[string]string{LabelKeyDefaultClass: LabelValueTrue}
r := &DefaultClassReconciler{
client: m.GetClient(),
converter: m.GetScheme(),
labels: labels,
newClaim: nc,
newPortableClass: np,
newPortableClassList: npl,
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile reconciles a claim to the default class reference for its kind.
func (r *DefaultClassReconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log.V(logging.Debug).Info("Reconciling", "request", req, "controller", controllerNameDefaultClass)
ctx, cancel := context.WithTimeout(context.Background(), defaultClassReconcileTimeout)
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.
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(err), errGetClaim)
}
// Get portable classes for claim kind in claim's namespace
portables := r.newPortableClassList()
if err := r.client.List(ctx, portables, client.InNamespace(req.Namespace), client.MatchingLabels(r.labels)); err != nil {
// If this is the first time we encounter listing error we'll be
// requeued implicitly due to the status update. If not, we don't
// care to requeue because list parameters will not change.
claim.SetConditions(corev1alpha1.ReconcileError(errors.New(errFailedList)))
return reconcile.Result{}, errors.Wrap(IgnoreNotFound(r.client.Status().Update(ctx, claim)), errUpdateClaimStatus)
}
items := portables.GetPortableClassItems()
// Check to see if no defaults defined for claim kind.
if len(items) == 0 {
// If this is the first time we encounter no default portable classes we'll be
// requeued implicitly due to the status update. If not, we will requeue
// after a time to see if a default portable class has been created.
claim.SetConditions(corev1alpha1.ReconcileError(errors.New(errNoPortableClass)))
return reconcile.Result{RequeueAfter: defaultClassWait}, errors.Wrap(IgnoreNotFound(r.client.Status().Update(ctx, claim)), errUpdateClaimStatus)
}
// Check to see if multiple defaults defined for claim kind.
if len(items) > 1 {
// If this is the first time we encounter multiple default portable classes we'll be
// requeued implicitly due to the status update. If not, we will requeue
// after a time to see if only one default portable class exists.
claim.SetConditions(corev1alpha1.ReconcileError(errors.New(errMultiplePortableClasses)))
return reconcile.Result{RequeueAfter: defaultClassWait}, errors.Wrap(IgnoreNotFound(r.client.Status().Update(ctx, claim)), errUpdateClaimStatus)
}
// Set portable class reference on claim to default portable class.
portable := items[0]
claim.SetPortableClassReference(&corev1.LocalObjectReference{Name: portable.GetName()})
// Do not requeue, claim controller will see update and claim
// with class reference set will pass predicates.
return reconcile.Result{Requeue: false}, errors.Wrap(IgnoreNotFound(r.client.Update(ctx, claim)), errUpdateClaimStatus)
}

View File

@ -1,266 +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 resource
import (
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/test"
)
var _ reconcile.Reconciler = &DefaultClassReconciler{}
type MockObjectConvertor struct {
runtime.ObjectConvertor
}
func (m *MockObjectConvertor) Convert(in, out, context interface{}) error {
i, inok := in.(*unstructured.Unstructured)
if !inok {
return errors.Errorf("expected conversion input to be of type %s", reflect.TypeOf(unstructured.Unstructured{}).String())
}
_, outok := out.(*MockPortableClass)
if !outok {
return errors.Errorf("expected conversion input to be of type %s", reflect.TypeOf(MockPortableClass{}).String())
}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(i.Object, out); err != nil {
return err
}
return nil
}
func TestDefaultClassReconcile(t *testing.T) {
type args struct {
m manager.Manager
of ClaimKind
by PortableClassKind
o []DefaultClassReconcilerOption
}
type want struct {
result reconcile.Result
err error
}
errBoom := errors.New("boom")
errUnexpected := errors.New("unexpected object type")
classRef := &corev1.ObjectReference{
Name: "default-class",
Namespace: "default-namespace",
}
portable := MockPortableClass{}
portable.SetNonPortableClassReference(classRef)
portableList := []PortableClass{
&MockPortableClass{},
}
portableListTooMany := []PortableClass{
&MockPortableClass{},
&MockPortableClass{},
}
cases := map[string]struct {
args args
want want
}{
"GetClaimError": {
args: args{
m: &MockManager{
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockPortableClassList{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
by: PortableClassKind{Singular: MockGVK(&MockPortableClass{}), Plural: MockGVK(&MockPortableClassList{})},
},
want: want{err: errors.Wrap(errBoom, errGetClaim)},
},
"ListPoliciesError": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockClaim:
*o = MockClaim{}
return nil
default:
return errUnexpected
}
}),
MockList: test.NewMockListFn(errBoom),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetConditions(v1alpha1.ReconcileError(errors.New(errFailedList)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockPortableClassList{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
by: PortableClassKind{Singular: MockGVK(&MockPortableClass{}), Plural: MockGVK(&MockPortableClassList{})},
},
want: want{result: reconcile.Result{}},
},
"NoDefaultClass": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockClaim:
*o = MockClaim{}
return nil
default:
return errUnexpected
}
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockPortableClassList:
*o = MockPortableClassList{}
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetConditions(v1alpha1.ReconcileError(errors.New(errNoPortableClass)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockPortableClassList{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
by: PortableClassKind{Singular: MockGVK(&MockPortableClass{}), Plural: MockGVK(&MockPortableClassList{})},
},
want: want{result: reconcile.Result{RequeueAfter: defaultClassWait}},
},
"MultipleDefaultClasses": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockClaim:
*o = MockClaim{}
return nil
default:
return errUnexpected
}
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockPortableClassList:
pl := &MockPortableClassList{}
pl.SetPortableClassItems(portableListTooMany)
*o = *pl
return nil
default:
return errUnexpected
}
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetConditions(v1alpha1.ReconcileError(errors.New(errMultiplePortableClasses)))
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockPortableClassList{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
by: PortableClassKind{Singular: MockGVK(&MockPortableClass{}), Plural: MockGVK(&MockPortableClassList{})},
},
want: want{result: reconcile.Result{RequeueAfter: defaultClassWait}},
},
"Successful": {
args: args{
m: &MockManager{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockClaim:
*o = MockClaim{}
return nil
default:
return errUnexpected
}
}),
MockList: test.NewMockListFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockPortableClassList:
pl := &MockPortableClassList{}
pl.SetPortableClassItems(portableList)
*o = *pl
return nil
default:
return errUnexpected
}
}),
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &MockClaim{}
want.SetPortableClassReference(&corev1.LocalObjectReference{Name: portable.GetName()})
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
return nil
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockPortableClassList{}),
},
of: ClaimKind(MockGVK(&MockClaim{})),
by: PortableClassKind{Singular: MockGVK(&MockPortableClass{}), Plural: MockGVK(&MockPortableClassList{})},
o: []DefaultClassReconcilerOption{WithObjectConverter(&MockObjectConvertor{})},
},
want: want{result: reconcile.Result{Requeue: false}},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewDefaultClassReconciler(tc.args.m, tc.args.of, tc.args.by, 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

@ -44,16 +44,16 @@ type ClaimReferencer interface {
GetClaimReference() *corev1.ObjectReference
}
// A NonPortableClassReferencer may reference a non-portable resource class.
type NonPortableClassReferencer interface {
SetNonPortableClassReference(r *corev1.ObjectReference)
GetNonPortableClassReference() *corev1.ObjectReference
// A ClassSelector may reference a resource class.
type ClassSelector interface {
SetClassSelector(s *metav1.LabelSelector)
GetClassSelector() *metav1.LabelSelector
}
// A PortableClassReferencer may reference a local portable class.
type PortableClassReferencer interface {
SetPortableClassReference(r *corev1.LocalObjectReference)
GetPortableClassReference() *corev1.LocalObjectReference
// 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.
@ -62,10 +62,18 @@ type ManagedResourceReferencer interface {
GetResourceReference() *corev1.ObjectReference
}
// A ConnectionSecretWriterTo may write a connection secret.
// A LocalConnectionSecretWriterTo may write a connection secret to its own
// namespace.
type LocalConnectionSecretWriterTo interface {
SetWriteConnectionSecretToReference(r *v1alpha1.LocalSecretReference)
GetWriteConnectionSecretToReference() *v1alpha1.LocalSecretReference
}
// A ConnectionSecretWriterTo may write a connection secret to an arbitrary
// namespace.
type ConnectionSecretWriterTo interface {
SetWriteConnectionSecretToReference(r corev1.LocalObjectReference)
GetWriteConnectionSecretToReference() corev1.LocalObjectReference
SetWriteConnectionSecretToReference(r *v1alpha1.SecretReference)
GetWriteConnectionSecretToReference() *v1alpha1.SecretReference
}
// A Reclaimer may specify a ReclaimPolicy.
@ -74,12 +82,6 @@ type Reclaimer interface {
GetReclaimPolicy() v1alpha1.ReclaimPolicy
}
// A PortableClassLister may contain a list of portable classes.
type PortableClassLister interface {
SetPortableClassItems(i []PortableClass)
GetPortableClassItems() []PortableClass
}
// 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).
@ -87,17 +89,18 @@ type Claim interface {
runtime.Object
metav1.Object
PortableClassReferencer
ClassSelector
ClassReferencer
ManagedResourceReferencer
ConnectionSecretWriterTo
LocalConnectionSecretWriterTo
Conditioned
Bindable
}
// A NonPortableClass is a Kubernetes object representing configuration
// specifications for a manged resource.
type NonPortableClass interface {
// A Class is a Kubernetes object representing configuration specifications for
// a managed resource.
type Class interface {
runtime.Object
metav1.Object
@ -110,7 +113,7 @@ type Managed interface {
runtime.Object
metav1.Object
NonPortableClassReferencer
ClassReferencer
ClaimReferencer
ConnectionSecretWriterTo
Reclaimer
@ -118,21 +121,3 @@ type Managed interface {
Conditioned
Bindable
}
// A PortableClass is a Kubernetes object representing a default
// behavior for a given claim kind.
type PortableClass interface {
runtime.Object
metav1.Object
NonPortableClassReferencer
}
// A PortableClassList is a Kubernetes object representing representing
// a list of portable classes.
type PortableClassList interface {
runtime.Object
metav1.ListInterface
PortableClassLister
}

View File

@ -41,21 +41,21 @@ type MockClaimReferencer struct{ Ref *corev1.ObjectReference }
func (m *MockClaimReferencer) SetClaimReference(r *corev1.ObjectReference) { m.Ref = r }
func (m *MockClaimReferencer) GetClaimReference() *corev1.ObjectReference { return m.Ref }
type MockNonPortableClassReferencer struct{ Ref *corev1.ObjectReference }
type MockClassSelector struct{ Sel *metav1.LabelSelector }
func (m *MockNonPortableClassReferencer) SetNonPortableClassReference(r *corev1.ObjectReference) {
func (m *MockClassSelector) SetClassSelector(s *metav1.LabelSelector) {
m.Sel = s
}
func (m *MockClassSelector) GetClassSelector() *metav1.LabelSelector {
return m.Sel
}
type MockClassReferencer struct{ Ref *corev1.ObjectReference }
func (m *MockClassReferencer) SetClassReference(r *corev1.ObjectReference) {
m.Ref = r
}
func (m *MockNonPortableClassReferencer) GetNonPortableClassReference() *corev1.ObjectReference {
return m.Ref
}
type MockPortableClassReferencer struct{ Ref *corev1.LocalObjectReference }
func (m *MockPortableClassReferencer) SetPortableClassReference(r *corev1.LocalObjectReference) {
m.Ref = r
}
func (m *MockPortableClassReferencer) GetPortableClassReference() *corev1.LocalObjectReference {
func (m *MockClassReferencer) GetClassReference() *corev1.ObjectReference {
return m.Ref
}
@ -64,12 +64,23 @@ type MockManagedResourceReferencer struct{ Ref *corev1.ObjectReference }
func (m *MockManagedResourceReferencer) SetResourceReference(r *corev1.ObjectReference) { m.Ref = r }
func (m *MockManagedResourceReferencer) GetResourceReference() *corev1.ObjectReference { return m.Ref }
type MockConnectionSecretWriterTo struct{ Ref corev1.LocalObjectReference }
type MockLocalConnectionSecretWriterTo struct {
Ref *v1alpha1.LocalSecretReference
}
func (m *MockConnectionSecretWriterTo) SetWriteConnectionSecretToReference(r corev1.LocalObjectReference) {
func (m *MockLocalConnectionSecretWriterTo) SetWriteConnectionSecretToReference(r *v1alpha1.LocalSecretReference) {
m.Ref = r
}
func (m *MockConnectionSecretWriterTo) GetWriteConnectionSecretToReference() corev1.LocalObjectReference {
func (m *MockLocalConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *v1alpha1.LocalSecretReference {
return m.Ref
}
type MockConnectionSecretWriterTo struct{ Ref *v1alpha1.SecretReference }
func (m *MockConnectionSecretWriterTo) SetWriteConnectionSecretToReference(r *v1alpha1.SecretReference) {
m.Ref = r
}
func (m *MockConnectionSecretWriterTo) GetWriteConnectionSecretToReference() *v1alpha1.SecretReference {
return m.Ref
}
@ -78,27 +89,23 @@ type MockReclaimer struct{ Policy v1alpha1.ReclaimPolicy }
func (m *MockReclaimer) SetReclaimPolicy(p v1alpha1.ReclaimPolicy) { m.Policy = p }
func (m *MockReclaimer) GetReclaimPolicy() v1alpha1.ReclaimPolicy { return m.Policy }
type MockPortableClassLister struct{ Items []PortableClass }
func (m *MockPortableClassLister) SetPortableClassItems(i []PortableClass) { m.Items = i }
func (m *MockPortableClassLister) GetPortableClassItems() []PortableClass { return m.Items }
var _ Claim = &MockClaim{}
type MockClaim struct {
runtime.Object
metav1.ObjectMeta
MockPortableClassReferencer
MockClassSelector
MockClassReferencer
MockManagedResourceReferencer
MockConnectionSecretWriterTo
MockLocalConnectionSecretWriterTo
MockConditioned
MockBindable
}
var _ NonPortableClass = &MockNonPortableClass{}
var _ Class = &MockClass{}
type MockNonPortableClass struct {
type MockClass struct {
runtime.Object
metav1.ObjectMeta
@ -111,28 +118,10 @@ type MockManaged struct {
runtime.Object
metav1.ObjectMeta
MockNonPortableClassReferencer
MockClassReferencer
MockClaimReferencer
MockConnectionSecretWriterTo
MockReclaimer
MockConditioned
MockBindable
}
var _ PortableClass = &MockPortableClass{}
type MockPortableClass struct {
runtime.Object
metav1.ObjectMeta
MockNonPortableClassReferencer
}
var _ PortableClassList = &MockPortableClassList{}
type MockPortableClassList struct {
runtime.Object
metav1.ListInterface
MockPortableClassLister
}

View File

@ -17,13 +17,9 @@ limitations under the License.
package resource
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
@ -83,6 +79,23 @@ 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 {
@ -158,59 +171,44 @@ func IsPropagated() PredicateFn {
}
}
// HasIndirectClassReferenceKind accepts namespaced objects that reference the
// supplied non-portable class kind via the supplied portable class kind.
func HasIndirectClassReferenceKind(c client.Client, oc runtime.ObjectCreater, k ClassKinds) PredicateFn {
// HasClassSelector accepts resource claims that do not specify a resource
// class selector.
func HasClassSelector() PredicateFn {
return func(obj runtime.Object) bool {
pcr, ok := obj.(PortableClassReferencer)
cs, ok := obj.(ClassSelector)
if !ok {
return false
}
pr := pcr.GetPortableClassReference()
if pr == nil {
return false
}
n, ok := obj.(interface{ GetNamespace() string })
if !ok {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), claimReconcileTimeout)
defer cancel()
portable := MustCreateObject(k.Portable, oc).(PortableClass)
p := types.NamespacedName{Namespace: n.GetNamespace(), Name: pr.Name}
if err := c.Get(ctx, p, portable); err != nil {
return false
}
cr := portable.GetNonPortableClassReference()
if cr == nil {
return false
}
gvk := cr.GroupVersionKind()
return gvk == k.NonPortable
return cs.GetClassSelector() != nil
}
}
// HasNoPortableClassReference accepts ResourceClaims that do not reference a
// specific portable class
func HasNoPortableClassReference() PredicateFn {
// HasNoClassSelector accepts resource claims that do not specify a resource
// class selector.
func HasNoClassSelector() PredicateFn {
return func(obj runtime.Object) bool {
cr, ok := obj.(PortableClassReferencer)
cs, ok := obj.(ClassSelector)
if !ok {
return false
}
return cr.GetPortableClassReference() == nil
return cs.GetClassSelector() == nil
}
}
// HasNoManagedResourceReference accepts ResourceClaims that do not reference a
// specific Managed Resource
// 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)

View File

@ -104,24 +104,20 @@ func TestHasManagedResourceReferenceKind(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
c client.Client
s runtime.ObjectCreater
kind ManagedKind
want bool
}{
"NotAClassReferencer": {
c: &test.MockClient{},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockManaged{}),
kind: ManagedKind(MockGVK(&MockManaged{})),
want: false,
},
"HasNoResourceReference": {
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockManaged{}),
obj: &MockClaim{},
kind: ManagedKind(MockGVK(&MockManaged{})),
want: false,
},
"HasCorrectResourceReference": {
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockManaged{}),
obj: &MockClaim{
MockManagedResourceReferencer: MockManagedResourceReferencer{
Ref: &corev1.ObjectReference{
@ -145,6 +141,47 @@ func TestHasManagedResourceReferenceKind(t *testing.T) {
}
}
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(MockGVK(&MockClass{})),
want: false,
},
"HasNoClassReference": {
obj: &MockClaim{},
kind: ClassKind(MockGVK(&MockClass{})),
want: false,
},
"HasCorrectClassReference": {
obj: &MockClaim{
MockClassReferencer: MockClassReferencer{
Ref: &corev1.ObjectReference{
APIVersion: MockGVK(&MockClass{}).GroupVersion().String(),
Kind: MockGVK(&MockClass{}).Kind,
},
},
},
kind: ClassKind(MockGVK(&MockClass{})),
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
@ -335,100 +372,63 @@ func TestIsPropagated(t *testing.T) {
}
}
func TestHasIndirectClassReferenceKind(t *testing.T) {
errUnexpected := errors.New("unexpected object type")
type withoutNamespace struct {
runtime.Object
*MockPortableClassReferencer
}
func TestHasClassSelector(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
c client.Client
s runtime.ObjectCreater
kind ClassKinds
want bool
}{
"NotAPortableClassReferencer": {
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
"NotAClassSelector": {
want: false,
},
"NoPortableClassReference": {
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
"NoClassSelector": {
obj: &MockClaim{},
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
want: false,
},
"NotANamespacer": {
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
obj: withoutNamespace{MockPortableClassReferencer: &MockPortableClassReferencer{Ref: &corev1.LocalObjectReference{}}},
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
want: false,
},
"GetPortableClassError": {
c: &test.MockClient{MockGet: test.NewMockGetFn(errors.New("boom"))},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
obj: &MockClaim{MockPortableClassReferencer: MockPortableClassReferencer{Ref: &corev1.LocalObjectReference{}}},
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
want: false,
},
"IncorrectNonPortableClassKind": {
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockPortableClass:
pc := &MockPortableClass{}
pc.SetNonPortableClassReference(&corev1.ObjectReference{})
*o = *pc
return nil
default:
return errUnexpected
}
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
obj: &MockClaim{MockPortableClassReferencer: MockPortableClassReferencer{Ref: &corev1.LocalObjectReference{}}},
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
want: false,
},
"CorrectNonPortableClassKind": {
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
switch o := o.(type) {
case *MockPortableClass:
pc := &MockPortableClass{}
version, kind := MockGVK(&MockNonPortableClass{}).ToAPIVersionAndKind()
pc.SetNonPortableClassReference(&corev1.ObjectReference{
Kind: kind,
APIVersion: version,
})
*o = *pc
return nil
default:
return errUnexpected
}
}),
},
s: MockSchemeWith(&MockClaim{}, &MockPortableClass{}, &MockNonPortableClass{}),
obj: &MockClaim{MockPortableClassReferencer: MockPortableClassReferencer{Ref: &corev1.LocalObjectReference{}}},
kind: ClassKinds{Portable: MockGVK(&MockPortableClass{}), NonPortable: MockGVK(&MockNonPortableClass{})},
"HasClassSelector": {
obj: &MockClaim{MockClassSelector: MockClassSelector{Sel: &v1.LabelSelector{}}},
want: true,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasIndirectClassReferenceKind(tc.c, tc.s, tc.kind)(tc.obj)
got := HasClassSelector()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("HasIndirectClassReferenceKind(...): -want, +got:\n%s", diff)
t.Errorf("HasClassSelector(...): -want, +got:\n%s", diff)
}
})
}
}
func TestHasNoPortableClassReference(t *testing.T) {
func TestHasNoClassSelector(t *testing.T) {
cases := map[string]struct {
obj runtime.Object
want bool
}{
"NotAClassSelector": {
want: false,
},
"NoClassSelector": {
obj: &MockClaim{},
want: true,
},
"HasClassSelector": {
obj: &MockClaim{MockClassSelector: MockClassSelector{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
@ -441,16 +441,16 @@ func TestHasNoPortableClassReference(t *testing.T) {
want: true,
},
"HasClassReference": {
obj: &MockClaim{MockPortableClassReferencer: MockPortableClassReferencer{Ref: &corev1.LocalObjectReference{}}},
obj: &MockClaim{MockClassReferencer: MockClassReferencer{Ref: &corev1.ObjectReference{}}},
want: false,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := HasNoPortableClassReference()(tc.obj)
got := HasNoClassReference()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("NoClassReference(...): -want, +got:\n%s", diff)
t.Errorf("HasNoClassReference(...): -want, +got:\n%s", diff)
}
})
}
@ -478,7 +478,7 @@ func TestHasNoMangedResourceReference(t *testing.T) {
t.Run(name, func(t *testing.T) {
got := HasNoManagedResourceReference()(tc.obj)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("NoManagedResourecReference(...): -want, +got:\n%s", diff)
t.Errorf("HasNoManagedResourecReference(...): -want, +got:\n%s", diff)
}
})
}

View File

@ -68,7 +68,7 @@ func NewAPISecretPublisher(c client.Client, ot runtime.ObjectTyper) *APISecretPu
// exists with the supplied ConnectionDetails.
func (a *APISecretPublisher) PublishConnection(ctx context.Context, mg Managed, c ConnectionDetails) error {
// This resource does not want to expose a connection secret.
if mg.GetWriteConnectionSecretToReference().Name == "" {
if mg.GetWriteConnectionSecretToReference() == nil {
return nil
}

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplaneio/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplaneio/crossplane-runtime/pkg/test"
)
@ -122,6 +123,7 @@ func TestAPISecretPublisher(t *testing.T) {
}
mgname := "coolmanaged"
mgcsnamespace := "coolnamespace"
mgcsname := "coolmanagedsecret"
mgcsdata := map[string][]byte{
"cool": []byte("data"),
@ -163,7 +165,10 @@ func TestAPISecretPublisher(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &MockManaged{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
Namespace: mgcsnamespace,
Name: mgcsname,
}},
},
c: ConnectionDetails{},
},
@ -183,7 +188,10 @@ func TestAPISecretPublisher(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &MockManaged{
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
Namespace: mgcsnamespace,
Name: mgcsname,
}},
},
c: ConnectionDetails{},
},
@ -195,6 +203,7 @@ func TestAPISecretPublisher(t *testing.T) {
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
MockCreate: test.NewMockCreateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{}
want.SetNamespace(mgcsnamespace)
want.SetName(mgcsname)
want.SetOwnerReferences([]metav1.OwnerReference{{
Name: mgname,
@ -214,8 +223,11 @@ func TestAPISecretPublisher(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
Namespace: mgcsnamespace,
Name: mgcsname,
}},
},
c: ConnectionDetails(cddata),
},
@ -226,6 +238,7 @@ func TestAPISecretPublisher(t *testing.T) {
client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := &corev1.Secret{}
s.SetNamespace(mgcsnamespace)
s.SetName(mgcsname)
s.SetOwnerReferences([]metav1.OwnerReference{{
Name: mgname,
@ -238,6 +251,7 @@ func TestAPISecretPublisher(t *testing.T) {
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{}
want.SetNamespace(mgcsnamespace)
want.SetName(mgcsname)
want.SetOwnerReferences([]metav1.OwnerReference{{
Name: mgname,
@ -257,8 +271,11 @@ func TestAPISecretPublisher(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
Namespace: mgcsnamespace,
Name: mgcsname,
}},
},
c: ConnectionDetails(cddata),
},
@ -269,6 +286,7 @@ func TestAPISecretPublisher(t *testing.T) {
client: &test.MockClient{
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
s := &corev1.Secret{}
s.SetNamespace(mgcsnamespace)
s.SetName(mgcsname)
s.SetOwnerReferences([]metav1.OwnerReference{{
Name: mgname,
@ -282,6 +300,7 @@ func TestAPISecretPublisher(t *testing.T) {
},
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
want := &corev1.Secret{}
want.SetNamespace(mgcsnamespace)
want.SetName(mgcsname)
want.SetOwnerReferences([]metav1.OwnerReference{{
Name: mgname,
@ -305,8 +324,11 @@ func TestAPISecretPublisher(t *testing.T) {
args: args{
ctx: context.Background(),
mg: &MockManaged{
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: corev1.LocalObjectReference{Name: mgcsname}},
ObjectMeta: metav1.ObjectMeta{Name: mgname},
MockConnectionSecretWriterTo: MockConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
Namespace: mgcsnamespace,
Name: mgcsname,
}},
},
c: ConnectionDetails(cddata),
},

View File

@ -28,18 +28,41 @@ import (
"github.com/crossplaneio/crossplane-runtime/pkg/meta"
)
// A ConnectionSecretOwner may create and manage a connection secret.
// A LocalConnectionSecretOwner may create and manage a connection secret in its
// own namespace.
type LocalConnectionSecretOwner interface {
metav1.Object
LocalConnectionSecretWriterTo
}
// LocalConnectionSecretFor creates a connection secret in the namespace of the
// supplied LocalConnectionSecretOwner, assumed to be of the supplied kind.
func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: o.GetNamespace(),
Name: o.GetWriteConnectionSecretToReference().Name,
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.ReferenceTo(o, kind))},
},
Data: make(map[string][]byte),
}
}
// A ConnectionSecretOwner may create and manage a connection secret in an
// arbitrary namespace.
type ConnectionSecretOwner interface {
metav1.Object
ConnectionSecretWriterTo
}
// ConnectionSecretFor the supplied ConnectionSecretOwner, assumed to be of the
// supplied kind.
// ConnectionSecretFor creates a connection for the supplied
// ConnectionSecretOwner, assumed to be of the supplied kind. The secret is
// written to 'default' namespace if the ConnectionSecretOwner does not specify
// a namespace.
func ConnectionSecretFor(o ConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: o.GetNamespace(),
Namespace: o.GetWriteConnectionSecretToReference().Namespace,
Name: o.GetWriteConnectionSecretToReference().Name,
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.ReferenceTo(o, kind))},
},

View File

@ -32,10 +32,9 @@ import (
)
const (
namespace = "coolns"
name = "cool"
secretName = "coolsecret"
uid = types.UID("definitely-a-uuid")
namespace = "coolns"
name = "cool"
uid = types.UID("definitely-a-uuid")
)
var MockOwnerGVK = schema.GroupVersionKind{
@ -44,17 +43,87 @@ var MockOwnerGVK = schema.GroupVersionKind{
Kind: "MockOwner",
}
type MockLocalOwner struct {
metav1.ObjectMeta
Ref *v1alpha1.LocalSecretReference
}
func (m *MockLocalOwner) GetWriteConnectionSecretToReference() *v1alpha1.LocalSecretReference {
return m.Ref
}
func (m *MockLocalOwner) SetWriteConnectionSecretToReference(r *v1alpha1.LocalSecretReference) {
m.Ref = r
}
func TestLocalConnectionSecretFor(t *testing.T) {
secretName := "coolsecret"
type args struct {
o LocalConnectionSecretOwner
kind schema.GroupVersionKind
}
controller := true
cases := map[string]struct {
args args
want *corev1.Secret
}{
"Success": {
args: args{
o: &MockLocalOwner{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
UID: uid,
},
Ref: &v1alpha1.LocalSecretReference{Name: secretName},
},
kind: MockOwnerGVK,
},
want: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
OwnerReferences: []metav1.OwnerReference{{
APIVersion: MockOwnerGVK.GroupVersion().String(),
Kind: MockOwnerGVK.Kind,
Name: name,
UID: uid,
Controller: &controller,
}},
},
Data: map[string][]byte{},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := LocalConnectionSecretFor(tc.args.o, tc.args.kind)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("LocalConnectionSecretFor(): -want, +got:\n%s", diff)
}
})
}
}
type MockOwner struct {
metav1.ObjectMeta
Ref *v1alpha1.SecretReference
}
func (m *MockOwner) GetWriteConnectionSecretToReference() corev1.LocalObjectReference {
return corev1.LocalObjectReference{Name: secretName}
func (m *MockOwner) GetWriteConnectionSecretToReference() *v1alpha1.SecretReference {
return m.Ref
}
func (m *MockOwner) SetWriteConnectionSecretToReference(_ corev1.LocalObjectReference) {}
func (m *MockOwner) SetWriteConnectionSecretToReference(r *v1alpha1.SecretReference) {
m.Ref = r
}
func TestConnectionSecretFor(t *testing.T) {
secretName := "coolsecret"
type args struct {
o ConnectionSecretOwner
kind schema.GroupVersionKind
@ -68,11 +137,14 @@ func TestConnectionSecretFor(t *testing.T) {
}{
"Success": {
args: args{
o: &MockOwner{ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
UID: uid,
}},
o: &MockOwner{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
UID: uid,
},
Ref: &v1alpha1.SecretReference{Namespace: namespace, Name: secretName},
},
kind: MockOwnerGVK,
},
want: &corev1.Secret{