@@ -4,9 +4,14 @@ import (
44 "context"
55 "fmt"
66 "reflect"
7+ "strings"
78
9+ apierrors "k8s.io/apimachinery/pkg/api/errors"
810 "k8s.io/apimachinery/pkg/runtime"
911 "k8s.io/apimachinery/pkg/runtime/schema"
12+ "k8s.io/apimachinery/pkg/util/sets"
13+ "k8s.io/client-go/rest"
14+ "k8s.io/client-go/util/csaupgrade"
1015 "sigs.k8s.io/controller-runtime/pkg/client"
1116 "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
1217 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -24,21 +29,59 @@ type SSAApplyClient struct {
2429
2530 // The field owner to use for SSA-applied objects.
2631 FieldOwner string
32+
33+ // MigrateSSAByDefault specifies the default SSA migration behavior.
34+ //
35+ // When checking for the migration, there is an additional GET of the resource, followed by optional
36+ // UPDATE (if the migration is needed) before the actual changes to the objects are applied.
37+ //
38+ // This field specifies the default behavior that can be overridden by supplying an explicit MigrateSSA() option
39+ // to ApplyObject or Apply methods.
40+ //
41+ // The main advantage of using the SSA in our code is that ability of SSA to handle automatic deletion of fields
42+ // that we no longer set in our templates. But this only works when the fields are owned by managers and applied
43+ // using "Apply" operation. As long as there is an "Update" entry with given field (even if the owner is the same)
44+ // the field WILL NOT be automatically deleted by Kubernetes.
45+ //
46+ // Therefore, we need to make sure that our manager uses ONLY the Apply operations. This maximizes the chance
47+ // that the object will look the way we need.
48+ MigrateSSAByDefault bool
49+
50+ // NonSSAFieldOwner should be set to the same value as the user agent used by the provided Kubernetes client
51+ // or to the value of the explicit field owner that the calling code used to use with the normal CRUD operations
52+ // (highly unlikely and not the case in our codebase).
53+ //
54+ // The user agent can be obtained from the REST config from which the client is constructed.
55+ //
56+ // The user agent in the REST config is usually empty, so there's no need to set it here either in that case.
57+ NonSSAFieldOwner string
2758}
2859
2960// NewSSAApplyClient creates a new SSAApplyClient from the provided parameters that will use the provided field owner
3061// for the patches.
62+ //
63+ // The returned client checks for the SSA migration by default.
3164func NewSSAApplyClient (cl client.Client , fieldOwner string ) * SSAApplyClient {
3265 return & SSAApplyClient {
33- Client : cl ,
34- FieldOwner : fieldOwner ,
66+ Client : cl ,
67+ FieldOwner : fieldOwner ,
68+ MigrateSSAByDefault : true ,
3569 }
3670}
3771
72+ type migrateSSA int
73+
74+ const (
75+ migrateSSANotSpecified migrateSSA = iota
76+ migrateSSAYes
77+ migrateSSANo
78+ )
79+
3880type ssaApplyObjectConfiguration struct {
39- owner metav1.Object
40- newLabels map [string ]string
41- skipIf func (client.Object ) bool
81+ owner metav1.Object
82+ newLabels map [string ]string
83+ skipIf func (client.Object ) bool
84+ migrateSSA migrateSSA
4285}
4386
4487func newSSAApplyObjectConfiguration (options ... SSAApplyObjectOption ) ssaApplyObjectConfiguration {
@@ -76,6 +119,19 @@ func EnsureLabels(labels map[string]string) SSAApplyObjectOption {
76119 }
77120}
78121
122+ // MigrateSSA instructs the apply to do the SSA managed fields migration or not.
123+ // If not used at all, the MigrateSSAByDefault field of the SSA client determines
124+ // whether the fields will be migrated or not.
125+ func MigrateSSA (value bool ) SSAApplyObjectOption {
126+ return func (config * ssaApplyObjectConfiguration ) {
127+ if value {
128+ config .migrateSSA = migrateSSAYes
129+ } else {
130+ config .migrateSSA = migrateSSANo
131+ }
132+ }
133+ }
134+
79135// Configure sets the owner reference and merges the labels. Other options modify the logic
80136// of apply function and therefore need to be checked manually.
81137func (c * ssaApplyObjectConfiguration ) Configure (obj client.Object , s * runtime.Scheme ) error {
@@ -100,6 +156,12 @@ func (c *SSAApplyClient) ApplyObject(ctx context.Context, obj client.Object, opt
100156 return composeError (obj , fmt .Errorf ("failed to prepare the object for SSA: %w" , err ))
101157 }
102158
159+ if config .migrateSSA == migrateSSAYes || (config .migrateSSA == migrateSSANotSpecified && c .MigrateSSAByDefault ) {
160+ if err := c .migrateSSA (ctx , obj ); err != nil {
161+ return composeError (obj , err )
162+ }
163+ }
164+
103165 if config .skipIf != nil && config .skipIf (obj ) {
104166 return nil
105167 }
@@ -111,6 +173,32 @@ func (c *SSAApplyClient) ApplyObject(ctx context.Context, obj client.Object, opt
111173 return nil
112174}
113175
176+ func (c * SSAApplyClient ) migrateSSA (ctx context.Context , obj client.Object ) error {
177+ orig := obj .DeepCopyObject ().(client.Object )
178+ if err := c .Client .Get (ctx , client .ObjectKeyFromObject (obj ), orig ); err != nil {
179+ if ! apierrors .IsNotFound (err ) {
180+ return fmt .Errorf ("failed to get the object from the cluster while migrating managed fields: %w" , err )
181+ }
182+ orig = nil
183+ }
184+
185+ if orig != nil {
186+ oldFieldOwner := c .NonSSAFieldOwner
187+ if len (oldFieldOwner ) == 0 {
188+ // this is how the kubernetes api server determines the default owner from the user agent
189+ // The default user agent has the form of "name-of-binary/version information etc.".
190+ // The owner is the first part of the UA unless explicitly specified in the request URI.
191+ oldFieldOwner = strings .Split (rest .DefaultKubernetesUserAgent (), "/" )[0 ]
192+ }
193+ if isSsaMigrationNeeded (orig , oldFieldOwner ) {
194+ if err := migrateToSSA (ctx , c .Client , orig , oldFieldOwner , c .FieldOwner ); err != nil {
195+ return fmt .Errorf ("failed to migrate the managed fields: %w" , err )
196+ }
197+ }
198+ }
199+ return nil
200+ }
201+
114202func composeError (obj client.Object , err error ) error {
115203 message := "unable to patch '%s' called '%s' in namespace '%s': %w"
116204 if ! obj .GetObjectKind ().GroupVersionKind ().Empty () {
@@ -157,3 +245,19 @@ func (c *SSAApplyClient) Apply(ctx context.Context, toolchainObjects []client.Ob
157245 }
158246 return nil
159247}
248+
249+ func isSsaMigrationNeeded (obj client.Object , expectedOwner string ) bool {
250+ for _ , mf := range obj .GetManagedFields () {
251+ if mf .Manager == expectedOwner && mf .Operation != metav1 .ManagedFieldsOperationApply {
252+ return true
253+ }
254+ }
255+ return false
256+ }
257+
258+ func migrateToSSA (ctx context.Context , cl client.Client , obj client.Object , oldFieldOwner , newFieldOwner string ) error {
259+ if err := csaupgrade .UpgradeManagedFields (obj , sets .New (oldFieldOwner ), newFieldOwner ); err != nil {
260+ return err
261+ }
262+ return cl .Update (ctx , obj )
263+ }
0 commit comments