Remove reconcilers and utilities pertaining to workloads
https://github.com/crossplane/crossplane/issues/1755 This removes support for the secret propagation and target controllers that each Kubernetes Cluster managed resource implemented before workloads were deprecated. See the above issue for context. Signed-off-by: Nic Cope <negz@rk0n.org>
This commit is contained in:
parent
ae2a3820ee
commit
defa322852
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Crossplane Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package secret provides a reconciler that propagates Kubernetes secrets.
|
|
||||||
package secret
|
|
|
@ -1,160 +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 secret
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
corev1 "k8s.io/api/core/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/pkg/event"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/logging"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
||||||
)
|
|
||||||
|
|
||||||
const secretReconcileTimeout = 1 * time.Minute
|
|
||||||
|
|
||||||
// Error messages.
|
|
||||||
const (
|
|
||||||
errGetSecret = "cannot get connection secret"
|
|
||||||
errUpdateSecret = "cannot update connection secret"
|
|
||||||
errPropagationNotAllowed = "the propagating connection secret does not allow propagation to the propagated connection secret"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event reasons
|
|
||||||
const (
|
|
||||||
reasonPropagatedFrom event.Reason = "PropagatedDataFrom"
|
|
||||||
reasonPropagatedTo event.Reason = "PropagatedDataTo"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ControllerName returns the recommended name for controllers that use this
|
|
||||||
// package to reconcile a particular kind of resource claim.
|
|
||||||
func ControllerName(kind string) string {
|
|
||||||
return "secretpropagating/" + strings.ToLower(kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconciler reconciles secrets by propagating their data from another secret.
|
|
||||||
// Both secrets must consent to this process by including propagation
|
|
||||||
// annotations. The Reconciler assumes it has a watch on both propagating (from)
|
|
||||||
// and propagated (to) secrets.
|
|
||||||
type Reconciler struct {
|
|
||||||
client client.Client
|
|
||||||
|
|
||||||
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 that reconciles secrets by propagating
|
|
||||||
// their data from another secret. Both secrets must consent to this process by
|
|
||||||
// including propagation annotations. The Reconciler assumes it has a watch on
|
|
||||||
// both propagating (from) and propagated (to) secrets.
|
|
||||||
func NewReconciler(m manager.Manager, o ...ReconcilerOption) *Reconciler {
|
|
||||||
r := &Reconciler{
|
|
||||||
client: m.GetClient(),
|
|
||||||
log: logging.NewNopLogger(),
|
|
||||||
record: event.NewNopRecorder(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ro := range o {
|
|
||||||
ro(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconcile a secret by propagating its data from another secret.
|
|
||||||
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(), secretReconcileTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// The 'to' secret is also known as the 'propagated' secret. We guard
|
|
||||||
// against abusers of the propagation process by requiring that both
|
|
||||||
// secrets consent to propagation by specifying each other's UID. We
|
|
||||||
// cannot know the UID of a secret that doesn't exist, so the propagated
|
|
||||||
// secret must be created outside of the propagation process.
|
|
||||||
to := &corev1.Secret{}
|
|
||||||
if err := r.client.Get(ctx, req.NamespacedName, to); err != nil {
|
|
||||||
// There's no propagation to be done if the secret we propagate to
|
|
||||||
// does not exist. We assume we have a watch on that secret and will
|
|
||||||
// be queued if/when it is created. Otherwise we'll be requeued
|
|
||||||
// implicitly because we return an error.
|
|
||||||
log.Debug("Cannot get propagated secret", "error", err)
|
|
||||||
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
record := r.record.WithAnnotations("to-namespace", to.GetNamespace(), "to-name", to.GetName())
|
|
||||||
log = log.WithValues("to-namespace", to.GetNamespace(), "to-name", to.GetName())
|
|
||||||
|
|
||||||
// The 'from' secret is also know as the 'propagating' secret.
|
|
||||||
from := &corev1.Secret{}
|
|
||||||
if err := r.client.Get(ctx, meta.AllowsPropagationFrom(to), from); err != nil {
|
|
||||||
// There's no propagation to be done if the secret we're propagating
|
|
||||||
// from does not exist. We assume we have a watch on that secret and
|
|
||||||
// will be queued if/when it is created. Otherwise we'll be requeued
|
|
||||||
// implicitly because we return an error.
|
|
||||||
log.Debug("Cannot get propagating secret", "error", err)
|
|
||||||
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetSecret)
|
|
||||||
}
|
|
||||||
|
|
||||||
record = record.WithAnnotations("from-namespace", from.GetNamespace(), "from-name", from.GetName())
|
|
||||||
log = log.WithValues("from-namespace", from.GetNamespace(), "from-name", from.GetName())
|
|
||||||
|
|
||||||
if allowed := meta.AllowsPropagationTo(from); !allowed[req.NamespacedName] {
|
|
||||||
// The propagating secret did not expect this propagated secret. We
|
|
||||||
// assume we have a watch on both secrets, and will be requeued if and
|
|
||||||
// when this situation is remedied.
|
|
||||||
log.Debug("Propagation not allowed")
|
|
||||||
return reconcile.Result{}, errors.New(errPropagationNotAllowed)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
to.Data = from.Data
|
|
||||||
|
|
||||||
// If our update was unsuccessful. Keep trying to update
|
|
||||||
// additional secrets but implicitly requeue when finished.
|
|
||||||
log.Debug("Propagated secret data")
|
|
||||||
record.Event(to, event.Normal(reasonPropagatedFrom, "Data propagated from secret"))
|
|
||||||
record.Event(from, event.Normal(reasonPropagatedTo, "Data propagated to secret"))
|
|
||||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Update(ctx, to), errUpdateSecret)
|
|
||||||
}
|
|
|
@ -1,262 +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 secret
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
||||||
|
|
||||||
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReconciler(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
m manager.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
type want struct {
|
|
||||||
result reconcile.Result
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
mg := &fake.Managed{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "coolmanaged"},
|
|
||||||
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: &v1alpha1.SecretReference{
|
|
||||||
Namespace: "coolns",
|
|
||||||
Name: "coolmanagedsecret",
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
tg := &fake.Target{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{Namespace: "coolns", Name: "cooltarget"},
|
|
||||||
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{Ref: &v1alpha1.LocalSecretReference{
|
|
||||||
Name: "cooltargetsecret",
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
from := resource.ConnectionSecretFor(mg, fake.GVK(mg))
|
|
||||||
to := resource.LocalConnectionSecretFor(tg, fake.GVK(tg))
|
|
||||||
|
|
||||||
fromData := map[string][]byte{"cool": {1}}
|
|
||||||
|
|
||||||
errBoom := errors.New("boom")
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
|
||||||
args args
|
|
||||||
want want
|
|
||||||
}{
|
|
||||||
"ToNotFound": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
return kerrors.NewNotFound(schema.GroupResource{}, "")
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"GetToError": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
return errBoom
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
err: errors.Wrap(errBoom, errGetSecret),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"FromNotFound": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), o.(metav1.Object))
|
|
||||||
case from.GetName():
|
|
||||||
return kerrors.NewNotFound(schema.GroupResource{}, from.GetName())
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"GetFromError": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), o.(metav1.Object))
|
|
||||||
case from.GetName():
|
|
||||||
return errBoom
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
err: errors.Wrap(errBoom, errGetSecret),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"PropagationNotAllowedError": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), o.(metav1.Object))
|
|
||||||
case from.GetName():
|
|
||||||
*o.(*corev1.Secret) = *from
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
return errors.New("called unexpectedly")
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
err: errors.New(errPropagationNotAllowed),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"UpdateToError": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
s := to.DeepCopy()
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), s)
|
|
||||||
*o.(*corev1.Secret) = *s
|
|
||||||
case from.GetName():
|
|
||||||
meta.AllowPropagation(o.(metav1.Object), to.DeepCopy())
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
return errBoom
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
err: errors.Wrap(errBoom, errUpdateSecret),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"SuccessfulSingle": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch n.Name {
|
|
||||||
case to.GetName():
|
|
||||||
s := to.DeepCopy()
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), s)
|
|
||||||
*o.(*corev1.Secret) = *s
|
|
||||||
case from.GetName():
|
|
||||||
s := from.DeepCopy()
|
|
||||||
s.Data = fromData
|
|
||||||
meta.AllowPropagation(s, to.DeepCopy())
|
|
||||||
*o.(*corev1.Secret) = *s
|
|
||||||
default:
|
|
||||||
return errors.New("unexpected secret name")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
want := to.DeepCopy()
|
|
||||||
want.Data = fromData
|
|
||||||
meta.AllowPropagation(from.DeepCopy(), want)
|
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
|
||||||
t.Errorf("-want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range cases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
r := NewReconciler(tc.args.m)
|
|
||||||
got, err := r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: to.GetNamespace(), Name: to.GetName()}})
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Crossplane Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package target provides a reconciler that propagates a Kubernetes cluster
|
|
||||||
// managed resource's connection secret to a Kubernetes target.
|
|
||||||
// Deprecated: See https://github.com/crossplane/crossplane/issues/1595
|
|
||||||
package target
|
|
|
@ -1,229 +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 target
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
||||||
|
|
||||||
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/logging"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
targetReconcileTimeout = 1 * time.Minute
|
|
||||||
aShortWait = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error strings
|
|
||||||
const (
|
|
||||||
errGetTarget = "unable to get Target"
|
|
||||||
errUpdateTarget = "unable to update Target"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Event reasons.
|
|
||||||
const (
|
|
||||||
reasonSetSecretRef event.Reason = "SetSecretRef"
|
|
||||||
reasonCannotGetManaged event.Reason = "CannotGetManaged"
|
|
||||||
reasonCannotPropagateSecret event.Reason = "CannotPropagateSecret"
|
|
||||||
reasonPropagatedSecret event.Reason = "PropagatedSecret"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TypeSecretPropagated resources have had connection information
|
|
||||||
// propagated to their secret reference.
|
|
||||||
const TypeSecretPropagated v1alpha1.ConditionType = "ConnectionSecretPropagated"
|
|
||||||
|
|
||||||
// NOTE(negz): This reconciler exists to service KubernetesApplications, which
|
|
||||||
// are deprecated per https://github.com/crossplane/crossplane/issues/1595. It
|
|
||||||
// had a PropagationSuccess condition but did not use it. I elected not to fix
|
|
||||||
// this due to its impending removal.
|
|
||||||
|
|
||||||
// ReasonSecretPropagationError indicates that secret propagation failed.
|
|
||||||
const ReasonSecretPropagationError v1alpha1.ConditionReason = "PropagationError"
|
|
||||||
|
|
||||||
// ControllerName returns the recommended name for controllers that use this
|
|
||||||
// package to reconcile a particular kind of target.
|
|
||||||
func ControllerName(kind string) string {
|
|
||||||
return "target/" + strings.ToLower(kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Reconciler reconciles targets by propagating the secret of the
|
|
||||||
// referenced managed resource.
|
|
||||||
type Reconciler struct {
|
|
||||||
client client.Client
|
|
||||||
newTarget func() resource.Target
|
|
||||||
newManaged func() resource.Managed
|
|
||||||
|
|
||||||
propagator resource.ManagedConnectionPropagator
|
|
||||||
|
|
||||||
log logging.Logger
|
|
||||||
record event.Recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// A ReconcilerOption configures a Reconciler.
|
|
||||||
type ReconcilerOption func(*Reconciler)
|
|
||||||
|
|
||||||
// WithManagedConnectionPropagator specifies which ManagedConnectionPropagator
|
|
||||||
// should be used to propagate resource connection details to their target.
|
|
||||||
func WithManagedConnectionPropagator(p resource.ManagedConnectionPropagator) ReconcilerOption {
|
|
||||||
return func(r *Reconciler) {
|
|
||||||
r.propagator = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogger specifies how the Reconciler should log messages.
|
|
||||||
func WithLogger(l logging.Logger) ReconcilerOption {
|
|
||||||
return func(r *Reconciler) {
|
|
||||||
r.log = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRecorder specifies how the Reconciler should record events.
|
|
||||||
func WithRecorder(er event.Recorder) ReconcilerOption {
|
|
||||||
return func(r *Reconciler) {
|
|
||||||
r.record = er
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReconciler returns a Reconciler that reconciles KubernetesTargets by
|
|
||||||
// propagating the referenced Kubernetes cluster's connection Secret to the
|
|
||||||
// namespace of the KubernetesTarget.
|
|
||||||
func NewReconciler(m manager.Manager, of resource.TargetKind, with resource.ManagedKind, o ...ReconcilerOption) *Reconciler {
|
|
||||||
nt := func() resource.Target {
|
|
||||||
return resource.MustCreateObject(schema.GroupVersionKind(of), m.GetScheme()).(resource.Target)
|
|
||||||
}
|
|
||||||
nr := func() resource.Managed {
|
|
||||||
return resource.MustCreateObject(schema.GroupVersionKind(with), m.GetScheme()).(resource.Managed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panic early if we've been asked to reconcile a target or resource kind
|
|
||||||
// that has not been registered with our controller manager's scheme.
|
|
||||||
_, _ = nt(), nr()
|
|
||||||
|
|
||||||
r := &Reconciler{
|
|
||||||
client: m.GetClient(),
|
|
||||||
newTarget: nt,
|
|
||||||
newManaged: nr,
|
|
||||||
|
|
||||||
// TODO(negz): Switch to resource.ConnectionPropagator after this has
|
|
||||||
// been deprecated for a release or two.
|
|
||||||
//nolint:staticcheck
|
|
||||||
propagator: resource.NewAPIManagedConnectionPropagator(m.GetClient(), m.GetScheme()),
|
|
||||||
|
|
||||||
log: logging.NewNopLogger(),
|
|
||||||
record: event.NewNopRecorder(),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ro := range o {
|
|
||||||
ro(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(negz): Could we use a regular ReconcileError instead of a bespoke
|
|
||||||
// SecretPropagationError for this controller?
|
|
||||||
|
|
||||||
// Reconcile a target with a concrete managed resource.
|
|
||||||
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(), targetReconcileTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
target := r.newTarget()
|
|
||||||
if err := r.client.Get(ctx, req.NamespacedName, target); err != nil {
|
|
||||||
// There's no need to requeue if we no longer exist. Otherwise we'll be
|
|
||||||
// requeued implicitly because we return an error.
|
|
||||||
log.Debug("Cannot get target", "error", err)
|
|
||||||
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Our watch predicates ensure we only reconcile targets with a non-nil
|
|
||||||
// resource reference.
|
|
||||||
record := r.record.WithAnnotations("managed-name", target.GetResourceReference().Name)
|
|
||||||
log = log.WithValues(
|
|
||||||
"uid", target.GetUID(),
|
|
||||||
"version", target.GetResourceVersion(),
|
|
||||||
"managed-name", target.GetResourceReference().Name,
|
|
||||||
)
|
|
||||||
|
|
||||||
if target.GetWriteConnectionSecretToReference() == nil {
|
|
||||||
// If the ConnectionSecretRef is not set on this Target, we generate a
|
|
||||||
// Secret name that matches the UID of the Target. We are implicitly
|
|
||||||
// requeued because of the Target update.
|
|
||||||
log.Debug("Set secret reference to target UID")
|
|
||||||
record.Event(target, event.Normal(reasonSetSecretRef, "Set secret reference to target UID"))
|
|
||||||
target.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: string(target.GetUID())})
|
|
||||||
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, target), errUpdateTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(negz): Move this above the secret ref check. Is this needed at all?
|
|
||||||
if meta.WasDeleted(target) {
|
|
||||||
// If the Target was deleted, there is nothing left for us to do.
|
|
||||||
return reconcile.Result{Requeue: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
managed := r.newManaged()
|
|
||||||
if err := r.client.Get(ctx, meta.NamespacedNameOf(target.GetResourceReference()), managed); err != nil {
|
|
||||||
log.Debug("Cannot get referenced managed resource", "error", err, "requeue-after", time.Now().Add(aShortWait))
|
|
||||||
record.Event(target, event.Warning(reasonCannotGetManaged, err))
|
|
||||||
target.SetConditions(SecretPropagationError(err))
|
|
||||||
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.propagator.PropagateConnection(ctx, target, managed); err != nil {
|
|
||||||
// If we fail to propagate the connection secret of a bound managed
|
|
||||||
// resource, we try again after a short wait.
|
|
||||||
log.Debug("Cannot propagate connection secret", "error", err, "requeue-after", time.Now().Add(aShortWait))
|
|
||||||
record.Event(target, event.Warning(reasonCannotPropagateSecret, err))
|
|
||||||
target.SetConditions(SecretPropagationError(err))
|
|
||||||
return reconcile.Result{RequeueAfter: aShortWait}, errors.Wrap(r.client.Status().Update(ctx, target), errUpdateTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need to requeue.
|
|
||||||
log.Debug("Successfully propagated connection secret")
|
|
||||||
record.Event(target, event.Normal(reasonPropagatedSecret, "Successfully propagated connection secret"))
|
|
||||||
return reconcile.Result{Requeue: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SecretPropagationError returns a condition indicating that Crossplane was
|
|
||||||
// unable to propagate connection data to the referenced secret. This could be
|
|
||||||
// because it was unable to find the managed resource that owns the secret to be
|
|
||||||
// propagated.
|
|
||||||
func SecretPropagationError(err error) v1alpha1.Condition {
|
|
||||||
return v1alpha1.Condition{
|
|
||||||
Type: TypeSecretPropagated,
|
|
||||||
Status: corev1.ConditionFalse,
|
|
||||||
LastTransitionTime: metav1.Now(),
|
|
||||||
Reason: ReasonSecretPropagationError,
|
|
||||||
Message: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,384 +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 target
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
||||||
|
|
||||||
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReconciler(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
m manager.Manager
|
|
||||||
of resource.TargetKind
|
|
||||||
with resource.ManagedKind
|
|
||||||
o []ReconcilerOption
|
|
||||||
}
|
|
||||||
|
|
||||||
type want struct {
|
|
||||||
result reconcile.Result
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
now := metav1.Now()
|
|
||||||
ns := "namespace"
|
|
||||||
tgname := "cooltarget"
|
|
||||||
mgname := "coolmanaged"
|
|
||||||
tguid := types.UID("tg-uuid")
|
|
||||||
mguid := types.UID("mg-uuid")
|
|
||||||
tgcsname := "cooltargetsecret"
|
|
||||||
mgcsname := "coolmanagedsecret"
|
|
||||||
mgcsnamespace := "coolns"
|
|
||||||
|
|
||||||
errBoom := errors.New("boom")
|
|
||||||
errUnexpected := errors.New("unexpected object type")
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
|
||||||
args args
|
|
||||||
want want
|
|
||||||
}{
|
|
||||||
"ErrorGetTarget": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
*o = fake.Target{}
|
|
||||||
return errBoom
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
err: errors.Wrap(errBoom, errGetTarget),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"SuccessTargetHasNoSecretRef": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: tguid,
|
|
||||||
Name: tgname,
|
|
||||||
Namespace: ns,
|
|
||||||
}}
|
|
||||||
tg.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
*o = *tg
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockUpdate: test.NewMockUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
want := &fake.Target{}
|
|
||||||
want.SetName(tgname)
|
|
||||||
want.SetNamespace(ns)
|
|
||||||
want.SetUID(tguid)
|
|
||||||
want.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
|
|
||||||
Name: string(tguid),
|
|
||||||
})
|
|
||||||
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
|
|
||||||
t.Errorf("-want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"TargetWasDeleted": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: tguid,
|
|
||||||
Name: tgname,
|
|
||||||
Namespace: ns,
|
|
||||||
}}
|
|
||||||
tg.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
|
|
||||||
Name: tgcsname,
|
|
||||||
})
|
|
||||||
tg.SetDeletionTimestamp(&now)
|
|
||||||
*o = *tg
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{Requeue: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"TargetNotFound": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
*o = fake.Target{}
|
|
||||||
return kerrors.NewNotFound(schema.GroupResource{}, "")
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ErrorGetManaged": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: tguid,
|
|
||||||
Name: tgname,
|
|
||||||
Namespace: ns,
|
|
||||||
}}
|
|
||||||
tg.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
|
|
||||||
Name: tgcsname,
|
|
||||||
})
|
|
||||||
*o = *tg
|
|
||||||
return nil
|
|
||||||
case *fake.Managed:
|
|
||||||
*o = fake.Managed{}
|
|
||||||
return errBoom
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
want := &fake.Target{}
|
|
||||||
want.SetName(tgname)
|
|
||||||
want.SetNamespace(ns)
|
|
||||||
want.SetUID(tguid)
|
|
||||||
want.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
|
|
||||||
want.SetConditions(SecretPropagationError(errBoom))
|
|
||||||
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
|
|
||||||
t.Errorf("-want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{RequeueAfter: aShortWait},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ErrorSecretPropagationFailed": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: tguid,
|
|
||||||
Name: tgname,
|
|
||||||
Namespace: ns,
|
|
||||||
}}
|
|
||||||
tg.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
|
|
||||||
Name: tgcsname,
|
|
||||||
})
|
|
||||||
*o = *tg
|
|
||||||
return nil
|
|
||||||
case *fake.Managed:
|
|
||||||
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: mguid,
|
|
||||||
Name: mgname,
|
|
||||||
}}
|
|
||||||
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
|
|
||||||
Name: mgcsname,
|
|
||||||
Namespace: mgcsnamespace,
|
|
||||||
})
|
|
||||||
*o = *mg
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MockStatusUpdate: test.NewMockStatusUpdateFn(nil, func(got runtime.Object) error {
|
|
||||||
want := &fake.Target{}
|
|
||||||
want.SetName(tgname)
|
|
||||||
want.SetNamespace(ns)
|
|
||||||
want.SetUID(tguid)
|
|
||||||
want.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
want.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{Name: tgcsname})
|
|
||||||
want.SetConditions(SecretPropagationError(errBoom))
|
|
||||||
if diff := cmp.Diff(want, got, test.EquateConditions()); diff != "" {
|
|
||||||
t.Errorf("-want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
o: []ReconcilerOption{
|
|
||||||
WithManagedConnectionPropagator(resource.ManagedConnectionPropagatorFn(
|
|
||||||
func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ resource.Managed) error {
|
|
||||||
return errBoom
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{RequeueAfter: aShortWait},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"Successful": {
|
|
||||||
args: args{
|
|
||||||
m: &fake.Manager{
|
|
||||||
Client: &test.MockClient{
|
|
||||||
MockGet: func(_ context.Context, n types.NamespacedName, o runtime.Object) error {
|
|
||||||
switch o := o.(type) {
|
|
||||||
case *fake.Target:
|
|
||||||
tg := &fake.Target{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: tguid,
|
|
||||||
Name: tgname,
|
|
||||||
Namespace: ns,
|
|
||||||
}}
|
|
||||||
tg.SetResourceReference(&corev1.ObjectReference{
|
|
||||||
Name: mgname,
|
|
||||||
})
|
|
||||||
tg.SetWriteConnectionSecretToReference(&v1alpha1.LocalSecretReference{
|
|
||||||
Name: tgcsname,
|
|
||||||
})
|
|
||||||
*o = *tg
|
|
||||||
return nil
|
|
||||||
case *fake.Managed:
|
|
||||||
mg := &fake.Managed{ObjectMeta: metav1.ObjectMeta{
|
|
||||||
UID: mguid,
|
|
||||||
Name: mgname,
|
|
||||||
}}
|
|
||||||
mg.SetWriteConnectionSecretToReference(&v1alpha1.SecretReference{
|
|
||||||
Name: mgcsname,
|
|
||||||
Namespace: mgcsnamespace,
|
|
||||||
})
|
|
||||||
*o = *mg
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return errUnexpected
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Scheme: fake.SchemeWith(&fake.Target{}, &fake.Managed{}),
|
|
||||||
},
|
|
||||||
of: resource.TargetKind(fake.GVK(&fake.Target{})),
|
|
||||||
with: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
|
||||||
o: []ReconcilerOption{
|
|
||||||
WithManagedConnectionPropagator(resource.ManagedConnectionPropagatorFn(
|
|
||||||
func(_ context.Context, _ resource.LocalConnectionSecretOwner, _ resource.Managed) error { return nil },
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: want{
|
|
||||||
result: reconcile.Result{Requeue: false},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range cases {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
r := NewReconciler(tc.args.m, tc.args.of, tc.args.with, tc.args.o...)
|
|
||||||
got, err := r.Reconcile(reconcile.Request{})
|
|
||||||
|
|
||||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
|
||||||
t.Errorf("r.Reconcile(...): -want error, +got error:\n%s", diff)
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff := cmp.Diff(tc.want.result, got); diff != "" {
|
|
||||||
t.Errorf("r.Reconcile(...): -want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -47,13 +47,6 @@ type APIManagedConnectionPropagator struct {
|
||||||
Propagator ConnectionPropagator
|
Propagator ConnectionPropagator
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIManagedConnectionPropagator returns a new APIConnectionPropagator.
|
|
||||||
//
|
|
||||||
// Deprecated: Use NewAPIConnectionPropagator.
|
|
||||||
func NewAPIManagedConnectionPropagator(c client.Client, t runtime.ObjectTyper) *APIManagedConnectionPropagator {
|
|
||||||
return &APIManagedConnectionPropagator{Propagator: NewAPIConnectionPropagator(c, t)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PropagateConnection details from the supplied resource.
|
// PropagateConnection details from the supplied resource.
|
||||||
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, to LocalConnectionSecretOwner, mg Managed) error {
|
func (a *APIManagedConnectionPropagator) PropagateConnection(ctx context.Context, to LocalConnectionSecretOwner, mg Managed) error {
|
||||||
return a.Propagator.PropagateConnection(ctx, to, mg)
|
return a.Propagator.PropagateConnection(ctx, to, mg)
|
||||||
|
|
|
@ -17,62 +17,17 @@ limitations under the License.
|
||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/client-go/util/workqueue"
|
"k8s.io/client-go/util/workqueue"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type adder interface {
|
type adder interface {
|
||||||
Add(item interface{})
|
Add(item interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnqueueRequestForPropagated enqueues a reconcile.Request for the
|
|
||||||
// NamespacedName of a propagated object, i.e. an object with propagation
|
|
||||||
// metadata annotations.
|
|
||||||
type EnqueueRequestForPropagated struct{}
|
|
||||||
|
|
||||||
// Create adds a NamespacedName for the supplied CreateEvent if its Object is
|
|
||||||
// propagated.
|
|
||||||
func (e *EnqueueRequestForPropagated) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
|
|
||||||
addPropagated(evt.Object, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update adds a NamespacedName for the supplied UpdateEvent if its Objects are
|
|
||||||
// propagated.
|
|
||||||
func (e *EnqueueRequestForPropagated) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
|
|
||||||
addPropagated(evt.ObjectOld, q)
|
|
||||||
addPropagated(evt.ObjectNew, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete adds a NamespacedName for the supplied DeleteEvent if its Object is
|
|
||||||
// propagated.
|
|
||||||
func (e *EnqueueRequestForPropagated) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
|
|
||||||
addPropagated(evt.Object, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic adds a NamespacedName for the supplied GenericEvent if its Object is
|
|
||||||
// propagated.
|
|
||||||
func (e *EnqueueRequestForPropagated) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
|
|
||||||
addPropagated(evt.Object, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addPropagated(obj runtime.Object, queue adder) {
|
|
||||||
o, ok := obj.(metav1.Object)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise we should enqueue a request for the objects it propagates to.
|
|
||||||
for nn := range meta.AllowsPropagationTo(o) {
|
|
||||||
queue.Add(reconcile.Request{NamespacedName: nn})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnqueueRequestForProviderConfig enqueues a reconcile.Request for a referenced
|
// EnqueueRequestForProviderConfig enqueues a reconcile.Request for a referenced
|
||||||
// ProviderConfig.
|
// ProviderConfig.
|
||||||
type EnqueueRequestForProviderConfig struct{}
|
type EnqueueRequestForProviderConfig struct{}
|
||||||
|
|
|
@ -20,19 +20,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
||||||
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ handler.EventHandler = &EnqueueRequestForPropagated{}
|
_ handler.EventHandler = &EnqueueRequestForProviderConfig{}
|
||||||
)
|
)
|
||||||
|
|
||||||
type addFn func(item interface{})
|
type addFn func(item interface{})
|
||||||
|
@ -41,46 +39,6 @@ func (fn addFn) Add(item interface{}) {
|
||||||
fn(item)
|
fn(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddPropagated(t *testing.T) {
|
|
||||||
ns := "coolns"
|
|
||||||
name := "coolname"
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
|
||||||
obj runtime.Object
|
|
||||||
queue adder
|
|
||||||
}{
|
|
||||||
"ObjectIsNotAnnotated": {
|
|
||||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
|
||||||
},
|
|
||||||
"ObjectMissingAnnotation": {
|
|
||||||
obj: &fake.Managed{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
|
||||||
"some.annotation": "some-value",
|
|
||||||
}}},
|
|
||||||
queue: addFn(func(_ interface{}) { t.Errorf("queue.Add() called unexpectedly") }),
|
|
||||||
},
|
|
||||||
"IsPropagator": {
|
|
||||||
obj: func() runtime.Object {
|
|
||||||
tg := &fake.Target{}
|
|
||||||
tg.SetNamespace(ns)
|
|
||||||
tg.SetName(name)
|
|
||||||
mg := &fake.Managed{}
|
|
||||||
meta.AllowPropagation(mg, tg)
|
|
||||||
return mg
|
|
||||||
}(),
|
|
||||||
queue: addFn(func(got interface{}) {
|
|
||||||
want := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: ns, Name: name}}
|
|
||||||
if diff := cmp.Diff(want, got); diff != "" {
|
|
||||||
t.Errorf("-want, +got:\n%s", diff)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
addPropagated(tc.obj, tc.queue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddProviderConfig(t *testing.T) {
|
func TestAddProviderConfig(t *testing.T) {
|
||||||
name := "coolname"
|
name := "coolname"
|
||||||
|
|
||||||
|
|
|
@ -243,30 +243,6 @@ func (m *Managed) DeepCopyObject() runtime.Object {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target is a mock that implements Target interface.
|
|
||||||
type Target struct {
|
|
||||||
metav1.ObjectMeta
|
|
||||||
ManagedResourceReferencer
|
|
||||||
LocalConnectionSecretWriterTo
|
|
||||||
v1alpha1.ConditionedStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectKind returns schema.ObjectKind.
|
|
||||||
func (m *Target) GetObjectKind() schema.ObjectKind {
|
|
||||||
return schema.EmptyObjectKind
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeepCopyObject returns a deep copy of Target as runtime.Object.
|
|
||||||
func (m *Target) DeepCopyObject() runtime.Object {
|
|
||||||
out := &Target{}
|
|
||||||
j, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal(j, out)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Composite is a mock that implements Composite interface.
|
// Composite is a mock that implements Composite interface.
|
||||||
type Composite struct {
|
type Composite struct {
|
||||||
metav1.ObjectMeta
|
metav1.ObjectMeta
|
||||||
|
|
|
@ -177,17 +177,6 @@ type ProviderConfigUsageList interface {
|
||||||
GetItems() []ProviderConfigUsage
|
GetItems() []ProviderConfigUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Target is a Kubernetes object that refers to credentials to connect
|
|
||||||
// to a deployment target. Target is a subset of the Claim interface.
|
|
||||||
type Target interface {
|
|
||||||
Object
|
|
||||||
|
|
||||||
LocalConnectionSecretWriterTo
|
|
||||||
ManagedResourceReferencer
|
|
||||||
|
|
||||||
Conditioned
|
|
||||||
}
|
|
||||||
|
|
||||||
// A Composite resource composes one or more Composed resources.
|
// A Composite resource composes one or more Composed resources.
|
||||||
type Composite interface {
|
type Composite interface {
|
||||||
Object
|
Object
|
||||||
|
|
|
@ -47,9 +47,6 @@ const (
|
||||||
// A ManagedKind contains the type metadata for a kind of managed resource.
|
// A ManagedKind contains the type metadata for a kind of managed resource.
|
||||||
type ManagedKind schema.GroupVersionKind
|
type ManagedKind schema.GroupVersionKind
|
||||||
|
|
||||||
// A TargetKind contains the type metadata for a kind of target resource.
|
|
||||||
type TargetKind schema.GroupVersionKind
|
|
||||||
|
|
||||||
// A CompositeKind contains the type metadata for a kind of composite resource.
|
// A CompositeKind contains the type metadata for a kind of composite resource.
|
||||||
type CompositeKind schema.GroupVersionKind
|
type CompositeKind schema.GroupVersionKind
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue