Skip to content

Commit fef0cd3

Browse files
author
Ankita Thomas
committed
moving server-side apply helper into shared library
1 parent f0031a9 commit fef0cd3

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

pkg/lib/util/util.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package util
2+
3+
import (
4+
"context"
5+
"fmt"
6+
operatorsv1 "github.com/operator-framework/api/pkg/operators/v1"
7+
operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
8+
operatorsv2alpha1 "github.com/operator-framework/api/pkg/operators/v2alpha1"
9+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
kscheme "k8s.io/client-go/kubernetes/scheme"
13+
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
14+
"reflect"
15+
controllerclient "sigs.k8s.io/controller-runtime/pkg/client"
16+
)
17+
18+
var (
19+
localSchemeBuilder = runtime.NewSchemeBuilder(
20+
kscheme.AddToScheme,
21+
apiextensionsv1.AddToScheme,
22+
apiregistrationv1.AddToScheme,
23+
operatorsv1alpha1.AddToScheme,
24+
operatorsv1.AddToScheme,
25+
operatorsv2alpha1.AddToScheme,
26+
)
27+
)
28+
29+
type Object interface {
30+
runtime.Object
31+
metav1.Object
32+
}
33+
34+
func SetDefaultGroupVersionKind(obj Object, s *runtime.Scheme) {
35+
gvk := obj.GetObjectKind().GroupVersionKind()
36+
if s == nil {
37+
s = runtime.NewScheme()
38+
_ = localSchemeBuilder.AddToScheme(s)
39+
}
40+
if gvk.Empty() && s != nil {
41+
// Best-effort guess the GVK
42+
gvks, _, err := s.ObjectKinds(obj)
43+
if err != nil {
44+
panic(fmt.Sprintf("unable to get gvks for object %T: %s", obj, err))
45+
}
46+
if len(gvks) == 0 || gvks[0].Empty() {
47+
panic(fmt.Sprintf("unexpected gvks registered for object %T: %v", obj, gvks))
48+
}
49+
// TODO: The same object can be registered for multiple group versions
50+
// (although in practise this doesn't seem to be used).
51+
// In such case, the version set may not be correct.
52+
gvk = gvks[0]
53+
}
54+
obj.GetObjectKind().SetGroupVersionKind(gvk)
55+
}
56+
57+
type ServerSideApplier struct {
58+
controllerclient.Client
59+
Scheme *runtime.Scheme
60+
}
61+
62+
// Apply returns a function that invokes a change func on an object and performs a server-side apply patch with the result and its status subresource.
63+
// The given resource must be a pointer to a struct that specifies its Name, Namespace, APIVersion, and Kind at minimum.
64+
// The given change function must be unary, matching the signature: "func(<obj type>) error".
65+
// The returned function is suitable for use w/ asyncronous assertions.
66+
// The underlying value of the given resource pointer is updated to reflect the latest cluster state each time the closure is successfully invoked.
67+
// Ex. Change the spec of an existing InstallPlan
68+
//
69+
// plan := &InstallPlan{}
70+
// plan.SetNamespace("ns")
71+
// plan.SetName("install-123def")
72+
// plan.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{
73+
// Group: GroupName,
74+
// Version: GroupVersion,
75+
// Kind: InstallPlanKind,
76+
// })
77+
// Eventually(c.Apply(plan, func(p *v1alpha1.InstallPlan) error {
78+
// p.Spec.Approved = true
79+
// return nil
80+
// })).Should(Succeed())
81+
func (client *ServerSideApplier) Apply(obj Object, changeFunc interface{}) func() error {
82+
// Ensure given object is a pointer
83+
objType := reflect.TypeOf(obj)
84+
if objType.Kind() != reflect.Ptr {
85+
panic(fmt.Sprintf("argument object must be a pointer"))
86+
}
87+
88+
// Ensure given function matches expected signature
89+
var (
90+
change = reflect.ValueOf(changeFunc)
91+
changeType = change.Type()
92+
)
93+
if n := changeType.NumIn(); n != 1 {
94+
panic(fmt.Sprintf("unexpected number of formal parameters in change function signature: expected 1, present %d", n))
95+
}
96+
if pt := changeType.In(0); pt.Kind() != reflect.Interface {
97+
if objType != pt {
98+
panic(fmt.Sprintf("argument object type does not match the change function parameter type: argument %s, parameter: %s", objType, pt))
99+
}
100+
} else if !objType.Implements(pt) {
101+
panic(fmt.Sprintf("argument object type does not implement the change function parameter type: argument %s, parameter: %s", objType, pt))
102+
}
103+
if n := changeType.NumOut(); n != 1 {
104+
panic(fmt.Sprintf("unexpected number of return values in change function signature: expected 1, present %d", n))
105+
}
106+
var err error
107+
if rt := changeType.Out(0); !rt.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
108+
panic(fmt.Sprintf("unexpected return type in change function signature: expected %t, present %s", err, rt))
109+
}
110+
111+
// Determine if we need to apply a status subresource
112+
// FIXME: use discovery client instead of relying on storage struct fields.
113+
_, applyStatus := objType.Elem().FieldByName("Status")
114+
115+
var (
116+
bg = context.Background()
117+
)
118+
key, err := controllerclient.ObjectKeyFromObject(obj)
119+
if err != nil {
120+
panic(fmt.Sprintf("unable to extract key from resource: %s", err))
121+
}
122+
123+
// Ensure the GVK is set before patching
124+
SetDefaultGroupVersionKind(obj, client.Scheme)
125+
126+
return func() error {
127+
changed := func(obj Object) (Object, error) {
128+
cp := obj.DeepCopyObject().(Object)
129+
if err := client.Get(bg, key, cp); err != nil {
130+
return nil, err
131+
}
132+
// Reset the GVK after the client call strips it
133+
SetDefaultGroupVersionKind(cp, client.Scheme)
134+
cp.SetManagedFields(nil)
135+
136+
out := change.Call([]reflect.Value{reflect.ValueOf(cp)})
137+
if len(out) != 1 {
138+
panic(fmt.Sprintf("unexpected number of return values from apply mutation func: expected 1, returned %d", len(out)))
139+
}
140+
141+
if err := out[0].Interface(); err != nil {
142+
return nil, err.(error)
143+
}
144+
145+
return cp, nil
146+
}
147+
148+
cp, err := changed(obj)
149+
if err != nil {
150+
return err
151+
}
152+
153+
if err := client.Patch(bg, cp, controllerclient.Apply, controllerclient.ForceOwnership, controllerclient.FieldOwner("test")); err != nil {
154+
fmt.Printf("first patch error: %s\n", err)
155+
return err
156+
}
157+
158+
if !applyStatus {
159+
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(cp).Elem())
160+
return nil
161+
}
162+
163+
cp, err = changed(cp)
164+
if err != nil {
165+
return err
166+
}
167+
168+
if err := client.Status().Patch(bg, cp, controllerclient.Apply, controllerclient.ForceOwnership, controllerclient.FieldOwner("test")); err != nil {
169+
fmt.Printf("second patch error: %s\n", err)
170+
return err
171+
}
172+
173+
reflect.ValueOf(obj).Elem().Set(reflect.ValueOf(cp).Elem())
174+
175+
return nil
176+
}
177+
}

0 commit comments

Comments
 (0)