Merge pull request #206 from negz/useful

Add machinery for tracking ProviderConfig usage
This commit is contained in:
Nic Cope 2020-10-01 15:40:39 -07:00 committed by GitHub
commit ac14aba9a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1115 additions and 36 deletions

View File

@ -44,6 +44,10 @@ const (
ResourceCredentialsSecretKubeconfigKey = "kubeconfig"
)
// LabelKeyProviderName is added to ProviderConfigUsages to relate them to their
// ProviderConfig.
const LabelKeyProviderName = "crossplane.io/provider-config"
// 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
@ -157,8 +161,8 @@ type ResourceStatus struct {
ConditionedStatus `json:",inline"`
}
// A ProviderSpec defines the common way to get to the necessary objects to connect
// to the provider.
// A ProviderSpec defines the common way to get to the necessary objects to
// connect to the provider.
// Deprecated: Please use ProviderConfigSpec.
type ProviderSpec struct {
// CredentialsSecretRef references a specific secret's key that contains
@ -167,8 +171,8 @@ type ProviderSpec struct {
CredentialsSecretRef *SecretKeySelector `json:"credentialsSecretRef,omitempty"`
}
// A ProviderConfigSpec defines the common way to get to the necessary objects to connect
// to the provider.
// A ProviderConfigSpec defines the common way to get to the necessary objects
// to connect to the provider.
type ProviderConfigSpec struct {
// CredentialsSecretRef references a specific secret's key that contains
// the credentials that are used to connect to the provider.
@ -176,6 +180,24 @@ type ProviderConfigSpec struct {
CredentialsSecretRef *SecretKeySelector `json:"credentialsSecretRef,omitempty"`
}
// A ProviderConfigStatus defines the observed status of a ProviderConfig.
type ProviderConfigStatus struct {
ConditionedStatus `json:",inline"`
// Users of this provider configuration.
Users int64 `json:"users,omitempty"`
}
// A ProviderConfigUsage is a record that a particular managed resource is using
// a particular provider configuration.
type ProviderConfigUsage struct {
// ProviderConfigReference to the provider config being used.
ProviderConfigReference Reference `json:"providerConfigRef"`
// ResourceReference to the managed resource using the provider config.
ResourceReference TypedReference `json:"resourceRef"`
}
// A TargetSpec defines the common fields of objects used for exposing
// infrastructure to workloads that can be scheduled to.
type TargetSpec struct {

View File

@ -97,6 +97,39 @@ func (in *ProviderConfigSpec) DeepCopy() *ProviderConfigSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) {
*out = *in
in.ConditionedStatus.DeepCopyInto(&out.ConditionedStatus)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigStatus.
func (in *ProviderConfigStatus) DeepCopy() *ProviderConfigStatus {
if in == nil {
return nil
}
out := new(ProviderConfigStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderConfigUsage) DeepCopyInto(out *ProviderConfigUsage) {
*out = *in
out.ProviderConfigReference = in.ProviderConfigReference
out.ResourceReference = in.ResourceReference
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigUsage.
func (in *ProviderConfigUsage) DeepCopy() *ProviderConfigUsage {
if in == nil {
return nil
}
out := new(ProviderConfigUsage)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
*out = *in

View File

@ -0,0 +1,219 @@
/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package providerconfig
import (
"context"
"strings"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
finalizer = "in-use.crossplane.io"
shortWait = 30 * time.Second
timeout = 2 * time.Minute
errGetPC = "cannot get ProviderConfig"
errListPCUs = "cannot list ProviderConfigUsages"
errDeletePCU = "cannot delete ProviderConfigUsage"
errUpdate = "cannot update ProviderConfig"
errUpdateStatus = "cannot update ProviderConfig status"
)
// Event reasons.
const (
reasonAccount event.Reason = "UsageAccounting"
)
// Condition types and reasons.
const (
TypeTerminating v1alpha1.ConditionType = "Terminating"
ReasonInUse v1alpha1.ConditionReason = "InUse"
)
// Terminating indicates a ProviderConfig has been deleted, but that the
// deletion is being blocked because it is still in use.
func Terminating() v1alpha1.Condition {
return v1alpha1.Condition{
Type: TypeTerminating,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Reason: ReasonInUse,
}
}
// ControllerName returns the recommended name for controllers that use this
// package to reconcile a particular kind of managed resource.
func ControllerName(kind string) string {
return "providerconfig/" + strings.ToLower(kind)
}
// A Reconciler reconciles managed resources by creating and managing the
// lifecycle of an external resource, i.e. a resource in an external system such
// as a cloud provider API. Each controller must watch the managed resource kind
// for which it is responsible.
type Reconciler struct {
client client.Client
newConfig func() resource.ProviderConfig
newUsageList func() resource.ProviderConfigUsageList
log logging.Logger
record event.Recorder
}
// A ReconcilerOption configures a Reconciler.
type ReconcilerOption func(*Reconciler)
// WithLogger specifies how the Reconciler should log messages.
func WithLogger(l logging.Logger) ReconcilerOption {
return func(r *Reconciler) {
r.log = l
}
}
// WithRecorder specifies how the Reconciler should record events.
func WithRecorder(er event.Recorder) ReconcilerOption {
return func(r *Reconciler) {
r.record = er
}
}
// NewReconciler returns a Reconciler of ProviderConfigs.
func NewReconciler(m manager.Manager, of resource.ProviderConfigKinds, o ...ReconcilerOption) *Reconciler {
nc := func() resource.ProviderConfig {
return resource.MustCreateObject(of.Config, m.GetScheme()).(resource.ProviderConfig)
}
nul := func() resource.ProviderConfigUsageList {
return resource.MustCreateObject(of.UsageList, m.GetScheme()).(resource.ProviderConfigUsageList)
}
// Panic early if we've been asked to reconcile a resource kind that has not
// been registered with our controller manager's scheme.
_, _ = nc(), nul()
r := &Reconciler{
client: m.GetClient(),
newConfig: nc,
newUsageList: nul,
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
}
for _, ro := range o {
ro(r)
}
return r
}
// Reconcile a ProviderConfig by accounting for the managed resources that are
// using it, and ensuring it cannot be deleted until it is no longer in use.
func (r *Reconciler) Reconcile(req reconcile.Request) (reconcile.Result, error) {
log := r.log.WithValues("request", req)
log.Debug("Reconciling")
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
pc := r.newConfig()
if err := r.client.Get(ctx, req.NamespacedName, pc); err != nil {
// In case object is not found, most likely the object was deleted and
// then disappeared while the event was in the processing queue. We
// don't need to take any action in that case.
log.Debug(errGetPC, "error", err)
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetPC)
}
log = log.WithValues(
"uid", pc.GetUID(),
"version", pc.GetResourceVersion(),
"name", pc.GetName(),
)
l := r.newUsageList()
if err := r.client.List(ctx, l, client.MatchingLabels{v1alpha1.LabelKeyProviderName: pc.GetName()}); err != nil {
log.Debug(errListPCUs, "error", err)
r.record.Event(pc, event.Warning(reasonAccount, errors.Wrap(err, errListPCUs)))
return reconcile.Result{RequeueAfter: shortWait}, nil
}
users := int64(len(l.GetItems()))
for _, pcu := range l.GetItems() {
if metav1.GetControllerOf(pcu) == nil {
// Usages should always have a controller reference. If this one has
// none it's probably been stripped off (e.g. by a Velero restore).
// We can safely delete it - it's either stale, or will be recreated
// next time the relevant managed resource connects.
if err := r.client.Delete(ctx, pcu); resource.IgnoreNotFound(err) != nil {
log.Debug(errDeletePCU, "error", err)
r.record.Event(pc, event.Warning(reasonAccount, errors.Wrap(err, errDeletePCU)))
return reconcile.Result{RequeueAfter: shortWait}, nil
}
users--
}
}
log = log.WithValues("usages", users)
if meta.WasDeleted(pc) {
if users > 0 {
msg := "Blocking deletion while usages still exist"
log.Debug(msg)
r.record.Event(pc, event.Warning(reasonAccount, errors.New(msg)))
// We're watching our usages, so we'll be requeued when they go.
pc.SetUsers(users)
pc.SetConditions(Terminating().WithMessage(msg))
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, pc), errUpdateStatus)
}
meta.RemoveFinalizer(pc, finalizer)
if err := r.client.Update(ctx, pc); err != nil {
r.log.Debug(errUpdate, "error", err)
return reconcile.Result{RequeueAfter: shortWait}, nil
}
// We've been deleted - there's no more work to do.
return reconcile.Result{Requeue: false}, nil
}
meta.AddFinalizer(pc, finalizer)
if err := r.client.Update(ctx, pc); err != nil {
r.log.Debug(errUpdate, "error", err)
return reconcile.Result{RequeueAfter: shortWait}, nil
}
// There's no need to requeue explicitly - we're watching all PCs.
pc.SetUsers(users)
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, pc), errUpdateStatus)
}

View File

@ -0,0 +1,331 @@
/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package providerconfig
import (
"encoding/json"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
// This can't live in fake, because it would cause an import cycle due to
// GetItems returning managed.ProviderConfigUsage.
type ProviderConfigUsageList struct {
Items []resource.ProviderConfigUsage
}
func (p *ProviderConfigUsageList) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
func (p *ProviderConfigUsageList) DeepCopyObject() runtime.Object {
out := &ProviderConfigUsageList{}
j, err := json.Marshal(p)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
func (p *ProviderConfigUsageList) GetItems() []resource.ProviderConfigUsage {
return p.Items
}
func TestReconciler(t *testing.T) {
errBoom := errors.New("boom")
now := metav1.Now()
uid := types.UID("so-unique")
ctrl := true
type args struct {
m manager.Manager
of resource.ProviderConfigKinds
}
type want struct {
result reconcile.Result
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"GetProviderConfigError": {
reason: "Errors getting a provider config should be returned",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{},
err: errors.Wrap(errBoom, errGetPC),
},
},
"ProviderConfigNotFound": {
reason: "We should return without requeueing if the provider config no longer exists",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{},
err: nil,
},
},
"ListProviderConfigUsageError": {
reason: "We should requeue after a short wait if we encounter an error listing provider config usages",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
},
},
"DeleteProviderConfigUsageError": {
reason: "We should requeue after a short wait if we encounter an error deleting a provider config usage",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil, func(obj runtime.Object) error {
l := obj.(*ProviderConfigUsageList)
l.Items = []resource.ProviderConfigUsage{
&fake.ProviderConfigUsage{},
}
return nil
}),
MockDelete: test.NewMockDeleteFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
},
},
"BlockDeleteWhileInUse": {
reason: "We should return without requeueing if the provider config is still in use",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
pc := obj.(*fake.ProviderConfig)
pc.SetDeletionTimestamp(&now)
pc.SetUID(uid)
return nil
}),
MockList: test.NewMockListFn(nil, func(obj runtime.Object) error {
l := obj.(*ProviderConfigUsageList)
l.Items = []resource.ProviderConfigUsage{
&fake.ProviderConfigUsage{
ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{{
UID: uid,
Controller: &ctrl,
}},
},
},
}
return nil
}),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{Requeue: false},
},
},
"RemoveFinalizerError": {
reason: "We should requeue after a short wait if we encounter an error while removing our finalizer",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
pc := obj.(*fake.ProviderConfig)
pc.SetDeletionTimestamp(&now)
return nil
}),
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
},
},
"SuccessfulDelete": {
reason: "We should return without requeueing when we successfully remove our finalizer",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj runtime.Object) error {
pc := obj.(*fake.ProviderConfig)
pc.SetDeletionTimestamp(&now)
return nil
}),
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{Requeue: false},
},
},
"AddFinalizerError": {
reason: "We should requeue after a short wait if we encounter an error while adding our finalizer",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{RequeueAfter: shortWait},
},
},
"UpdateStatusError": {
reason: "We return errors encountered while updating our status",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(errBoom),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{Requeue: false},
err: errors.Wrap(errBoom, errUpdateStatus),
},
},
"SuccessfulSetUsers": {
reason: "We should return without requeuing if we successfully update our user count",
args: args{
m: &fake.Manager{
Client: &test.MockClient{
MockGet: test.NewMockGetFn(nil),
MockList: test.NewMockListFn(nil),
MockUpdate: test.NewMockUpdateFn(nil),
MockStatusUpdate: test.NewMockStatusUpdateFn(nil),
},
Scheme: fake.SchemeWith(&fake.ProviderConfig{}, &ProviderConfigUsageList{}),
},
of: resource.ProviderConfigKinds{
Config: fake.GVK(&fake.ProviderConfig{}),
UsageList: fake.GVK(&ProviderConfigUsageList{}),
},
},
want: want{
result: reconcile.Result{Requeue: false},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
r := NewReconciler(tc.args.m, tc.args.of)
got, err := r.Reconcile(reconcile.Request{})
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nr.Reconcile(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.result, got); diff != "" {
t.Errorf("\n%s\nr.Reconcile(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -19,6 +19,7 @@ package resource
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/workqueue"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@ -71,3 +72,41 @@ func addPropagated(obj runtime.Object, queue adder) {
queue.Add(reconcile.Request{NamespacedName: nn})
}
}
// EnqueueRequestForProviderConfig enqueues a reconcile.Request for a referenced
// ProviderConfig.
type EnqueueRequestForProviderConfig struct{}
// Create adds a NamespacedName for the supplied CreateEvent if its Object is a
// ProviderConfigReferencer.
func (e *EnqueueRequestForProviderConfig) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
addProviderConfig(evt.Object, q)
}
// Update adds a NamespacedName for the supplied UpdateEvent if its Objects are
// a ProviderConfigReferencer.
func (e *EnqueueRequestForProviderConfig) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
addProviderConfig(evt.ObjectOld, q)
addProviderConfig(evt.ObjectNew, q)
}
// Delete adds a NamespacedName for the supplied DeleteEvent if its Object is a
// ProviderConfigReferencer.
func (e *EnqueueRequestForProviderConfig) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
addProviderConfig(evt.Object, q)
}
// Generic adds a NamespacedName for the supplied GenericEvent if its Object is
// a ProviderConfigReferencer.
func (e *EnqueueRequestForProviderConfig) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
addProviderConfig(evt.Object, q)
}
func addProviderConfig(obj runtime.Object, queue adder) {
pcr, ok := obj.(RequiredProviderConfigReferencer)
if !ok {
return
}
queue.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: pcr.GetProviderConfigReference().Name}})
}

View File

@ -26,6 +26,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
)
@ -79,3 +80,33 @@ func TestAddPropagated(t *testing.T) {
addPropagated(tc.obj, tc.queue)
}
}
func TestAddProviderConfig(t *testing.T) {
name := "coolname"
cases := map[string]struct {
obj runtime.Object
queue adder
}{
"NotProviderConfigReferencer": {
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
},
"IsProviderConfigReferencer": {
obj: &fake.ProviderConfigUsage{
RequiredProviderConfigReferencer: fake.RequiredProviderConfigReferencer{
Ref: v1alpha1.Reference{Name: name},
},
},
queue: addFn(func(got interface{}) {
want := reconcile.Request{NamespacedName: types.NamespacedName{Name: name}}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("-want, +got:\n%s", diff)
}
}),
},
}
for _, tc := range cases {
addProviderConfig(tc.obj, tc.queue)
}
}

View File

@ -80,6 +80,34 @@ func (m *ProviderConfigReferencer) SetProviderConfigReference(p *v1alpha1.Refere
// GetProviderConfigReference gets the ProviderConfigReference.
func (m *ProviderConfigReferencer) GetProviderConfigReference() *v1alpha1.Reference { return m.Ref }
// RequiredProviderConfigReferencer is a mock that implements the
// RequiredProviderConfigReferencer interface.
type RequiredProviderConfigReferencer struct{ Ref v1alpha1.Reference }
// SetProviderConfigReference sets the ProviderConfigReference.
func (m *RequiredProviderConfigReferencer) SetProviderConfigReference(p v1alpha1.Reference) {
m.Ref = p
}
// GetProviderConfigReference gets the ProviderConfigReference.
func (m *RequiredProviderConfigReferencer) GetProviderConfigReference() v1alpha1.Reference {
return m.Ref
}
// RequiredTypedResourceReferencer is a mock that implements the
// RequiredTypedResourceReferencer interface.
type RequiredTypedResourceReferencer struct{ Ref v1alpha1.TypedReference }
// SetResourceReference sets the ResourceReference.
func (m *RequiredTypedResourceReferencer) SetResourceReference(p v1alpha1.TypedReference) {
m.Ref = p
}
// GetResourceReference gets the ResourceReference.
func (m *RequiredTypedResourceReferencer) GetResourceReference() v1alpha1.TypedReference {
return m.Ref
}
// LocalConnectionSecretWriterTo is a mock that implements LocalConnectionSecretWriterTo interface.
type LocalConnectionSecretWriterTo struct {
Ref *v1alpha1.LocalSecretReference
@ -119,15 +147,15 @@ func (m *Orphanable) GetDeletionPolicy() v1alpha1.DeletionPolicy { return m.Poli
// CredentialsSecretReferencer is a mock that satisfies CredentialsSecretReferencer
// interface.
type CredentialsSecretReferencer struct{ Ref v1alpha1.SecretKeySelector }
type CredentialsSecretReferencer struct{ Ref *v1alpha1.SecretKeySelector }
// SetCredentialsSecretReference sets CredentialsSecretReference.
func (m *CredentialsSecretReferencer) SetCredentialsSecretReference(r v1alpha1.SecretKeySelector) {
func (m *CredentialsSecretReferencer) SetCredentialsSecretReference(r *v1alpha1.SecretKeySelector) {
m.Ref = r
}
// GetCredentialsSecretReference gets CredentialsSecretReference.
func (m *CredentialsSecretReferencer) GetCredentialsSecretReference() v1alpha1.SecretKeySelector {
func (m *CredentialsSecretReferencer) GetCredentialsSecretReference() *v1alpha1.SecretKeySelector {
return m.Ref
}
@ -167,6 +195,20 @@ func (m *ComposedResourcesReferencer) SetResourceReferences(r []corev1.ObjectRef
// GetResourceReferences gets the composed references.
func (m *ComposedResourcesReferencer) GetResourceReferences() []corev1.ObjectReference { return m.Refs }
// UserCounter is a mock that satisfies UserCounter
// interface.
type UserCounter struct{ Users int64 }
// SetUsers sets the count of users.
func (m *UserCounter) SetUsers(i int64) {
m.Users = i
}
// GetUsers gets the count of users.
func (m *UserCounter) GetUsers() int64 {
return m.Users
}
// Object is a mock that implements Object interface.
type Object struct {
metav1.ObjectMeta
@ -215,28 +257,6 @@ func (m *Managed) DeepCopyObject() runtime.Object {
return out
}
// Provider is a mock that satisfies Provider interface.
type Provider struct {
metav1.ObjectMeta
CredentialsSecretReferencer
}
// GetObjectKind returns schema.ObjectKind.
func (m *Provider) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a deep copy of Provider as runtime.Object.
func (m *Provider) DeepCopyObject() runtime.Object {
out := &Provider{}
j, err := json.Marshal(m)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
// Target is a mock that implements Target interface.
type Target struct {
metav1.ObjectMeta
@ -450,3 +470,54 @@ func (m *MockLocalConnectionSecretOwner) DeepCopyObject() runtime.Object {
_ = json.Unmarshal(j, out)
return out
}
// ProviderConfig is a mock implementation of the ProviderConfig interface.
type ProviderConfig struct {
metav1.ObjectMeta
CredentialsSecretReferencer
UserCounter
v1alpha1.ConditionedStatus
}
// GetObjectKind returns schema.ObjectKind.
func (p *ProviderConfig) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a copy of the object as runtime.Object
func (p *ProviderConfig) DeepCopyObject() runtime.Object {
out := &ProviderConfig{}
j, err := json.Marshal(p)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}
// ProviderConfigUsage is a mock implementation of the ProviderConfigUsage
// interface.
type ProviderConfigUsage struct {
metav1.ObjectMeta
RequiredProviderConfigReferencer
RequiredTypedResourceReferencer
}
// GetObjectKind returns schema.ObjectKind.
func (p *ProviderConfigUsage) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
// DeepCopyObject returns a copy of the object as runtime.Object
func (p *ProviderConfigUsage) DeepCopyObject() runtime.Object {
out := &ProviderConfigUsage{}
j, err := json.Marshal(p)
if err != nil {
panic(err)
}
_ = json.Unmarshal(j, out)
return out
}

View File

@ -68,8 +68,8 @@ type Orphanable interface {
// A CredentialsSecretReferencer may refer to a credential secret in an arbitrary
// namespace.
type CredentialsSecretReferencer interface {
GetCredentialsSecretReference() v1alpha1.SecretKeySelector
SetCredentialsSecretReference(r v1alpha1.SecretKeySelector)
GetCredentialsSecretReference() *v1alpha1.SecretKeySelector
SetCredentialsSecretReference(r *v1alpha1.SecretKeySelector)
}
// A ProviderReferencer may reference a provider resource.
@ -84,6 +84,19 @@ type ProviderConfigReferencer interface {
SetProviderConfigReference(p *v1alpha1.Reference)
}
// A RequiredProviderConfigReferencer may reference a provider config resource.
// Unlike ProviderConfigReferencer, the reference is required (i.e. not nil).
type RequiredProviderConfigReferencer interface {
GetProviderConfigReference() v1alpha1.Reference
SetProviderConfigReference(p v1alpha1.Reference)
}
// A RequiredTypedResourceReferencer can reference a resource.
type RequiredTypedResourceReferencer interface {
SetResourceReference(r v1alpha1.TypedReference)
GetResourceReference() v1alpha1.TypedReference
}
// A Finalizer manages the finalizers on the resource.
type Finalizer interface {
AddFinalizer(ctx context.Context, obj Object) error
@ -114,6 +127,12 @@ type CompositeResourceReferencer interface {
GetResourceReference() *corev1.ObjectReference
}
// A UserCounter can count how many users it has.
type UserCounter interface {
SetUsers(i int64)
GetUsers() int64
}
// An Object is a Kubernetes object.
type Object interface {
metav1.Object
@ -141,12 +160,30 @@ type ManagedList interface {
GetItems() []Managed
}
// A Provider is a Kubernetes object that refers to credentials to connect
// to an external system.
type Provider interface {
// A ProviderConfig configures a Crossplane provider.
type ProviderConfig interface {
Object
CredentialsSecretReferencer
UserCounter
Conditioned
}
// A ProviderConfigUsage indicates a usage of a Crossplane provider config.
type ProviderConfigUsage interface {
Object
RequiredProviderConfigReferencer
RequiredTypedResourceReferencer
}
// A ProviderConfigUsageList is a list of provider config usages.
type ProviderConfigUsageList interface {
runtime.Object
// GetItems returns the list of provider config usages.
GetItems() []ProviderConfigUsage
}
// A Target is a Kubernetes object that refers to credentials to connect

View File

@ -0,0 +1,91 @@
/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
)
const (
errMissingPCRef = "managed resource does not reference a ProviderConfig"
errApplyPCU = "cannot apply ProviderConfigUsage"
)
type errMissingRef struct{ error }
func (m errMissingRef) MissingReference() bool { return true }
// IsMissingReference returns true if an error indicates that a managed
// resource is missing a required reference..
func IsMissingReference(err error) bool {
_, ok := err.(interface {
MissingReference() bool
})
return ok
}
// A ProviderConfigUsageTracker tracks usages of a ProviderConfig by creating or
// updating the appropriate ProviderConfigUsage.
type ProviderConfigUsageTracker struct {
c Applicator
of ProviderConfigUsage
}
// NewProviderConfigUsageTracker creates a ProviderConfigUsageTracker.
func NewProviderConfigUsageTracker(c client.Client, of ProviderConfigUsage) *ProviderConfigUsageTracker {
return &ProviderConfigUsageTracker{c: NewAPIUpdatingApplicator(c), of: of}
}
// Track that the supplied Managed resource is using the ProviderConfig it
// references by creating or updating a ProviderConfigUsage. Track should be
// called _before_ attempting to use the ProviderConfig. This ensures the
// managed resource's usage is updated if the managed resource is updated to
// reference a misconfigured ProviderConfig.
func (u *ProviderConfigUsageTracker) Track(ctx context.Context, mg Managed) error {
pcu := u.of.DeepCopyObject().(ProviderConfigUsage)
gvk := mg.GetObjectKind().GroupVersionKind()
ref := mg.GetProviderConfigReference()
if ref == nil {
return errMissingRef{errors.New(errMissingPCRef)}
}
pcu.SetName(string(mg.GetUID()))
pcu.SetLabels(map[string]string{v1alpha1.LabelKeyProviderName: ref.Name})
pcu.SetOwnerReferences([]metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(mg, gvk))})
pcu.SetProviderConfigReference(v1alpha1.Reference{Name: ref.Name})
pcu.SetResourceReference(v1alpha1.TypedReference{
APIVersion: gvk.GroupVersion().String(),
Kind: gvk.Kind,
Name: mg.GetName(),
})
err := u.c.Apply(ctx, pcu,
MustBeControllableBy(mg.GetUID()),
AllowUpdateIf(func(current, _ runtime.Object) bool {
return current.(ProviderConfigUsage).GetProviderConfigReference() != pcu.GetProviderConfigReference()
}),
)
return errors.Wrap(Ignore(IsNotAllowed, err), errApplyPCU)
}

View File

@ -0,0 +1,123 @@
/*
Copyright 2020 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resource
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
func TestTrack(t *testing.T) {
errBoom := errors.New("boom")
name := "provisional"
type fields struct {
c Applicator
of ProviderConfigUsage
}
type args struct {
ctx context.Context
mg Managed
}
cases := map[string]struct {
reason string
fields fields
args args
want error
}{
"MissingRef": {
reason: "An error that satisfies IsMissingReference should be returned if the managed resource has no provider config reference",
fields: fields{
of: &fake.ProviderConfigUsage{},
},
args: args{
mg: &fake.Managed{},
},
want: errMissingRef{errors.New(errMissingPCRef)},
},
"NopUpdate": {
reason: "No error should be returned if the apply fails because it would be a no-op",
fields: fields{
c: ApplyFn(func(c context.Context, r runtime.Object, ao ...ApplyOption) error {
for _, fn := range ao {
// Exercise the MustBeControllableBy and AllowUpdateIf
// ApplyOptions. The former should pass because the
// current object has no controller ref. The latter
// should return an error that satisfies IsNotAllowed
// because the current object has the same PC ref as the
// new one we would apply.
current := &fake.ProviderConfigUsage{
RequiredProviderConfigReferencer: fake.RequiredProviderConfigReferencer{
Ref: v1alpha1.Reference{Name: name},
},
}
if err := fn(context.TODO(), current, nil); err != nil {
return err
}
}
return errBoom
}),
of: &fake.ProviderConfigUsage{},
},
args: args{
mg: &fake.Managed{
ProviderConfigReferencer: fake.ProviderConfigReferencer{
Ref: &v1alpha1.Reference{Name: name},
},
},
},
want: nil,
},
"ApplyError": {
reason: "Errors applying the ProviderConfigUsage should be returned",
fields: fields{
c: ApplyFn(func(c context.Context, r runtime.Object, ao ...ApplyOption) error {
return errBoom
}),
of: &fake.ProviderConfigUsage{},
},
args: args{
mg: &fake.Managed{
ProviderConfigReferencer: fake.ProviderConfigReferencer{
Ref: &v1alpha1.Reference{Name: name},
},
},
},
want: errors.Wrap(errBoom, errApplyPCU),
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ut := &ProviderConfigUsageTracker{c: tc.fields.c, of: tc.fields.of}
got := ut.Track(tc.args.ctx, tc.args.mg)
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nut.Track(...): -want error, +got error:\n%s\n", tc.reason, diff)
}
})
}
}

View File

@ -57,6 +57,13 @@ type CompositeKind schema.GroupVersionKind
// resource claim.
type CompositeClaimKind schema.GroupVersionKind
// ProviderConfigKinds contains the type metadata for a kind of provider config.
type ProviderConfigKinds struct {
Config schema.GroupVersionKind
Usage schema.GroupVersionKind
UsageList schema.GroupVersionKind
}
// A LocalConnectionSecretOwner may create and manage a connection secret in its
// own namespace.
type LocalConnectionSecretOwner interface {
@ -244,7 +251,9 @@ func IsNotControllable(err error) bool {
// MustBeControllableBy requires that the current object is controllable by an
// object with the supplied UID. An object is controllable if its controller
// reference matches the supplied UID, or it has no controller reference.
// reference matches the supplied UID, or it has no controller reference. An
// error that satisfies IsNotControllable will be returned if the current object
// cannot be controlled by the supplied UID.
func MustBeControllableBy(u types.UID) ApplyOption {
return func(_ context.Context, current, _ runtime.Object) error {
c := metav1.GetControllerOf(current.(metav1.Object))
@ -269,7 +278,9 @@ func MustBeControllableBy(u types.UID) ApplyOption {
// only considered controllable if they are already controlled by the supplied
// UID. It is not safe to assume legacy connection secrets without a controller
// reference are controllable because they are indistinguishable from Kubernetes
// secrets that have nothing to do with Crossplane.
// secrets that have nothing to do with Crossplane. An error that satisfies
// IsNotControllable will be returned if the current secret is not a connection
// secret or cannot be controlled by the supplied UID.
func ConnectionSecretMustBeControllableBy(u types.UID) ApplyOption {
return func(_ context.Context, current, _ runtime.Object) error {
s := current.(*corev1.Secret)
@ -302,6 +313,34 @@ func ControllersMustMatch() ApplyOption {
}
}
type errNotAllowed struct{ error }
func (e errNotAllowed) NotAllowed() bool {
return true
}
// IsNotAllowed returns true if the supplied error indicates that an operation
// was not allowed.
func IsNotAllowed(err error) bool {
_, ok := err.(interface {
NotAllowed() bool
})
return ok
}
// AllowUpdateIf will only update the current object if the supplied fn returns
// true. An error that satisfies IsNotAllowed will be returned if the supplied
// function returns false. Creation of a desired object that does not currently
// exist is always allowed.
func AllowUpdateIf(fn func(current, desired runtime.Object) bool) ApplyOption {
return func(_ context.Context, current, desired runtime.Object) error {
if fn(current, desired) {
return nil
}
return errNotAllowed{errors.New("update not allowed")}
}
}
// Apply changes to the supplied object. The object will be created if it does
// not exist, or patched if it does.
//

View File

@ -499,6 +499,7 @@ func TestMustBeControllableBy(t *testing.T) {
})
}
}
func TestConnectionSecretMustBeControllableBy(t *testing.T) {
uid := types.UID("very-unique-string")
controller := true
@ -571,6 +572,48 @@ func TestConnectionSecretMustBeControllableBy(t *testing.T) {
}
}
func TestAllowUpdateIf(t *testing.T) {
type args struct {
ctx context.Context
current runtime.Object
desired runtime.Object
}
cases := map[string]struct {
reason string
fn func(current, desired runtime.Object) bool
args args
want error
}{
"Allowed": {
reason: "No error should be returned when the supplied function returns true",
fn: func(current, desired runtime.Object) bool { return true },
args: args{
current: &object{},
},
},
"NotAllowed": {
reason: "An error that satisfies IsNotAllowed should be returned when the supplied function returns false",
fn: func(current, desired runtime.Object) bool { return false },
args: args{
current: &object{},
},
want: errNotAllowed{errors.New("update not allowed")},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ao := AllowUpdateIf(tc.fn)
err := ao(tc.args.ctx, tc.args.current, tc.args.desired)
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nAllowUpdateIf(...)(...): -want error, +got error\n%s\n", tc.reason, diff)
}
})
}
}
func TestGetExternalTags(t *testing.T) {
provName := "prov"
cases := map[string]struct {