Add an Apply object function
This function either creates a new object or patches an existing one. I think it belongs in crossplane-runtime because we have at least two implementations in the wild, and I need another one for a new controller. https://github.com/crossplane/templating-controller/blob/a035e2/pkg/controllers/templating_reconciler.go#L168 https://github.com/crossplane/crossplane/blob/c1933feab/pkg/controller/workload/kubernetes/resource/resource.go#L260 Signed-off-by: Nic Cope <negz@rk0n.org>
This commit is contained in:
parent
ca61a42964
commit
6ef3f96332
|
|
@ -18,6 +18,7 @@ package resource
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -25,6 +26,8 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
|
|
@ -227,3 +230,57 @@ func IsBound(b Bindable) bool {
|
|||
func IsConditionTrue(c v1alpha1.Condition) bool {
|
||||
return c.Status == corev1.ConditionTrue
|
||||
}
|
||||
|
||||
// ApplyOptions configure how changes are applied to an object.
|
||||
type ApplyOptions struct {
|
||||
// ControllersMustMatch requires any existing object to have a controller
|
||||
// reference, and for that controller reference to match the controller
|
||||
// reference of the supplied object.
|
||||
ControllersMustMatch bool
|
||||
}
|
||||
|
||||
// An ApplyOption configures how changes are applied to an object.
|
||||
type ApplyOption func(a *ApplyOptions)
|
||||
|
||||
// ControllersMustMatch requires any existing object to have a controller
|
||||
// reference, and for that controller reference to match the controller
|
||||
// reference of the supplied object.
|
||||
func ControllersMustMatch() ApplyOption {
|
||||
return func(a *ApplyOptions) {
|
||||
a.ControllersMustMatch = true
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes to the supplied object. The object will be created if it does
|
||||
// not exist, or patched if it does.
|
||||
func Apply(ctx context.Context, c client.Client, o runtime.Object, ao ...ApplyOption) error {
|
||||
opts := &ApplyOptions{}
|
||||
for _, fn := range ao {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
existing := o.DeepCopyObject()
|
||||
m, ok := existing.(metav1.Object)
|
||||
if !ok {
|
||||
return errors.New("cannot access object metadata")
|
||||
}
|
||||
|
||||
err := c.Get(ctx, types.NamespacedName{Name: m.GetName(), Namespace: m.GetNamespace()}, existing)
|
||||
if kerrors.IsNotFound(err) {
|
||||
return errors.Wrap(c.Create(ctx, o), "cannot create object")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get object")
|
||||
}
|
||||
|
||||
if opts.ControllersMustMatch && !meta.HaveSameController(o.(metav1.Object), m) {
|
||||
return errors.New("existing object has a different (or no) controller")
|
||||
}
|
||||
|
||||
return errors.Wrap(c.Patch(ctx, existing, &patch{o}), "cannot patch object")
|
||||
}
|
||||
|
||||
type patch struct{ from runtime.Object }
|
||||
|
||||
func (p *patch) Type() types.PatchType { return types.MergePatchType }
|
||||
func (p *patch) Data(_ runtime.Object) ([]byte, error) { return json.Marshal(p.from) }
|
||||
|
|
|
|||
|
|
@ -17,17 +17,21 @@ limitations under the License.
|
|||
package resource
|
||||
|
||||
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/client"
|
||||
|
||||
"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/test"
|
||||
)
|
||||
|
|
@ -440,3 +444,119 @@ func TestIsConditionTrue(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type object struct {
|
||||
runtime.Object
|
||||
metav1.ObjectMeta
|
||||
}
|
||||
|
||||
func (o *object) DeepCopyObject() runtime.Object {
|
||||
return &object{ObjectMeta: *o.ObjectMeta.DeepCopy()}
|
||||
}
|
||||
|
||||
type nopeject struct {
|
||||
runtime.Object
|
||||
}
|
||||
|
||||
func (o *nopeject) DeepCopyObject() runtime.Object {
|
||||
return &nopeject{}
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
errBoom := errors.New("boom")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
c client.Client
|
||||
o runtime.Object
|
||||
ao []ApplyOption
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
"NotAMetadataObject": {
|
||||
reason: "An error should be returned if we can't access the object's metadata",
|
||||
args: args{
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
|
||||
o: &nopeject{},
|
||||
},
|
||||
want: errors.New("cannot access object metadata"),
|
||||
},
|
||||
"GetError": {
|
||||
reason: "An error should be returned if we can't get the object",
|
||||
args: args{
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
|
||||
o: &object{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, "cannot get object"),
|
||||
},
|
||||
"CreateError": {
|
||||
reason: "No error should be returned if we successfully create a new object",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
|
||||
MockCreate: test.NewMockCreateFn(errBoom),
|
||||
},
|
||||
o: &object{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, "cannot create object"),
|
||||
},
|
||||
"ControllerMismatch": {
|
||||
reason: "An error should be returned if controllers must match, but don't",
|
||||
args: args{
|
||||
c: &test.MockClient{MockGet: test.NewMockGetFn(nil, func(o runtime.Object) error {
|
||||
obj := &object{}
|
||||
meta.AddControllerReference(obj, metav1.OwnerReference{UID: types.UID("wat")})
|
||||
*(o.(*object)) = *obj
|
||||
return nil
|
||||
})},
|
||||
o: &object{},
|
||||
ao: []ApplyOption{ControllersMustMatch()},
|
||||
},
|
||||
want: errors.New("existing object has a different (or no) controller"),
|
||||
},
|
||||
"PatchError": {
|
||||
reason: "An error should be returned if we can't patch the object",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockPatch: test.NewMockPatchFn(errBoom),
|
||||
},
|
||||
o: &object{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, "cannot patch object"),
|
||||
},
|
||||
"Created": {
|
||||
reason: "No error should be returned if we successfully create a new object",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, "")),
|
||||
MockCreate: test.NewMockCreateFn(nil),
|
||||
},
|
||||
o: &object{},
|
||||
},
|
||||
},
|
||||
"Patched": {
|
||||
reason: "No error should be returned if we successfully patch an existing object",
|
||||
args: args{
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockPatch: test.NewMockPatchFn(nil),
|
||||
},
|
||||
o: &object{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := Apply(tc.args.ctx, tc.args.c, tc.args.o, tc.args.ao...)
|
||||
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nApply(...): -want error, +got error\n%s\n", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue