Merge pull request #206 from negz/useful
Add machinery for tracking ProviderConfig usage
This commit is contained in:
commit
ac14aba9a2
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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}})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue