Skip to content

Commit 60628a0

Browse files
author
Ankita Thomas
committed
moving server-side apply helper into shared library
1 parent 2bf2fc7 commit 60628a0

File tree

9 files changed

+250
-158
lines changed

9 files changed

+250
-158
lines changed

pkg/lib/util/ssa.go

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

pkg/lib/util/ssa_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package util
2+
3+
import (
4+
"gotest.tools/assert"
5+
corev1 "k8s.io/api/core/v1"
6+
"k8s.io/apimachinery/pkg/runtime"
7+
"k8s.io/apimachinery/pkg/runtime/schema"
8+
"testing"
9+
)
10+
11+
func TestSharedTime(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
s *runtime.Scheme
15+
obj Object
16+
want schema.GroupVersionKind
17+
}{
18+
{
19+
name: "DefaultGVK",
20+
s: nil,
21+
obj: &corev1.ServiceAccount{},
22+
want: schema.GroupVersionKind{
23+
Group: "",
24+
Version: "v1",
25+
Kind: "ServiceAccount",
26+
},
27+
},
28+
}
29+
for _, tt := range tests {
30+
t.Run(tt.name, func(t *testing.T) {
31+
SetDefaultGroupVersionKind(tt.obj, tt.s)
32+
assert.DeepEqual(t, tt.obj.GetObjectKind().GroupVersionKind(), tt.want)
33+
})
34+
}
35+
}
36+

test/e2e/crd_e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ var _ = Describe("CRD Versions", func() {
302302
// old CRD has been installed onto the cluster - now upgrade the subscription to point to the channel with the new CRD
303303
// installing the new CSV should fail with a warning about data loss, since a storage version is missing in the new CRD
304304
// use server-side apply to apply the update to the subscription point to the alpha channel
305-
Eventually(Apply(subscription, func(s *operatorsv1alpha1.Subscription) error {
305+
Eventually(ctx.Ctx().ServerSideApplier().Apply(subscription, func(s *operatorsv1alpha1.Subscription) error {
306306
s.Spec.Channel = alphaChannel
307307
return nil
308308
})).Should(Succeed())

test/e2e/csv_e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ var _ = Describe("CSV", func() {
10151015
require.True(GinkgoT(), ok, "expected olm sha annotation not present on existing pod template")
10161016

10171017
// Induce a cert rotation
1018-
Eventually(Apply(fetchedCSV, func(csv *v1alpha1.ClusterServiceVersion) error {
1018+
Eventually(ctx.Ctx().ServerSideApplier().Apply(fetchedCSV, func(csv *v1alpha1.ClusterServiceVersion) error {
10191019
now := metav1.Now()
10201020
csv.Status.CertsLastUpdated = &now
10211021
csv.Status.CertsRotateAt = &now

test/e2e/ctx/ctx.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ctx
22

33
import (
44
"fmt"
5+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/util"
56
"strings"
67

78
. "github.com/onsi/ginkgo"
@@ -30,6 +31,7 @@ type TestContext struct {
3031
operatorClient versioned.Interface
3132
dynamicClient dynamic.Interface
3233
packageClient pversioned.Interface
34+
ssaClient *util.ServerSideApplier
3335

3436
scheme *runtime.Scheme
3537

@@ -80,6 +82,10 @@ func (ctx TestContext) Client() controllerclient.Client {
8082
return ctx.client
8183
}
8284

85+
func (ctx TestContext) ServerSideApplier() *util.ServerSideApplier {
86+
return ctx.ssaClient
87+
}
88+
8389
func setDerivedFields(ctx *TestContext) error {
8490
if ctx == nil {
8591
return fmt.Errorf("nil test context")
@@ -134,5 +140,10 @@ func setDerivedFields(ctx *TestContext) error {
134140
}
135141
ctx.client = client
136142

143+
ctx.ssaClient = &util.ServerSideApplier{
144+
Client: ctx.client,
145+
Scheme: ctx.scheme,
146+
Owner: "test",
147+
}
137148
return nil
138149
}

test/e2e/operator_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ var _ = Describe("Operator", func() {
146146
m.SetLabels(map[string]string{expectedKey: ""})
147147
return nil
148148
}
149-
Eventually(Apply(nsA, setComponentLabel)).Should(Succeed())
149+
Eventually(ctx.Ctx().ServerSideApplier().Apply(nsA, setComponentLabel)).Should(Succeed())
150150

151151
// Ensure o's status.components.refs field eventually contains a reference to ns-a
152152
By("eventually listing a single component reference")
@@ -176,16 +176,16 @@ var _ = Describe("Operator", func() {
176176
}
177177

178178
// Label sa-a and sa-b with o's component label
179-
Eventually(Apply(saA, setComponentLabel)).Should(Succeed())
180-
Eventually(Apply(saB, setComponentLabel)).Should(Succeed())
179+
Eventually(ctx.Ctx().ServerSideApplier().Apply(saA, setComponentLabel)).Should(Succeed())
180+
Eventually(ctx.Ctx().ServerSideApplier().Apply(saB, setComponentLabel)).Should(Succeed())
181181

182182
// Ensure o's status.components.refs field eventually contains references to sa-a and sa-b
183183
By("eventually listing multiple component references")
184184
componentRefEventuallyExists(w, true, getReference(scheme, saA))
185185
componentRefEventuallyExists(w, true, getReference(scheme, saB))
186186

187187
// Remove the component label from sa-b
188-
Eventually(Apply(saB, func(m metav1.Object) error {
188+
Eventually(ctx.Ctx().ServerSideApplier().Apply(saB, func(m metav1.Object) error {
189189
m.SetLabels(nil)
190190
return nil
191191
})).Should(Succeed())

test/e2e/subscription_e2e_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -954,14 +954,14 @@ var _ = Describe("Subscription", func() {
954954
plan.SetName(ref.Name)
955955

956956
// Set the InstallPlan's approval mode to Manual
957-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
957+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
958958
p.Spec.Approval = v1alpha1.ApprovalManual
959959
p.Spec.Approved = false
960960
return nil
961961
})).Should(Succeed())
962962

963963
// Set the InstallPlan's phase to None
964-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
964+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
965965
p.Status.Phase = v1alpha1.InstallPlanPhaseNone
966966
return nil
967967
})).Should(Succeed())
@@ -974,7 +974,7 @@ var _ = Describe("Subscription", func() {
974974
Expect(err).ToNot(HaveOccurred())
975975

976976
// Set the phase to InstallPlanPhaseRequiresApproval
977-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
977+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
978978
p.Status.Phase = v1alpha1.InstallPlanPhaseRequiresApproval
979979
return nil
980980
})).Should(Succeed())
@@ -987,7 +987,7 @@ var _ = Describe("Subscription", func() {
987987
Expect(err).ToNot(HaveOccurred())
988988

989989
// Set the phase to InstallPlanPhaseInstalling
990-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
990+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
991991
p.Status.Phase = v1alpha1.InstallPlanPhaseInstalling
992992
return nil
993993
})).Should(Succeed())
@@ -1000,7 +1000,7 @@ var _ = Describe("Subscription", func() {
10001000
Expect(err).ToNot(HaveOccurred())
10011001

10021002
// Set the phase to InstallPlanPhaseFailed and remove all status conditions
1003-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
1003+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
10041004
p.Status.Phase = v1alpha1.InstallPlanPhaseFailed
10051005
p.Status.Conditions = nil
10061006
return nil
@@ -1014,7 +1014,7 @@ var _ = Describe("Subscription", func() {
10141014
Expect(err).ToNot(HaveOccurred())
10151015

10161016
// Set status condition of type Installed to false with reason InstallComponentFailed
1017-
Eventually(Apply(plan, func(p *v1alpha1.InstallPlan) error {
1017+
Eventually(ctx.Ctx().ServerSideApplier().Apply(plan, func(p *v1alpha1.InstallPlan) error {
10181018
p.Status.Phase = v1alpha1.InstallPlanPhaseFailed
10191019
failedCond := p.Status.GetCondition(v1alpha1.InstallPlanInstalled)
10201020
failedCond.Status = corev1.ConditionFalse

0 commit comments

Comments
 (0)