Skip to content

Commit 95cf792

Browse files
authored
Add migration support to the SSA client (#480)
1 parent c883496 commit 95cf792

2 files changed

Lines changed: 197 additions & 5 deletions

File tree

pkg/client/ssa_client.go

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
3164
func 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+
3880
type 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

4487
func 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.
81137
func (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+
114202
func 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+
}

pkg/client/ssa_client_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
stderrors "errors"
66
"fmt"
7+
"strings"
78
"testing"
89

910
"github.com/codeready-toolchain/toolchain-common/pkg/client"
@@ -16,6 +17,8 @@ import (
1617
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1718
"k8s.io/apimachinery/pkg/runtime"
1819
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"k8s.io/client-go/rest"
21+
"k8s.io/utils/ptr"
1922
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
2023
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2124
)
@@ -154,6 +157,91 @@ func TestSsaClient(t *testing.T) {
154157
inCluster := &corev1.ConfigMap{}
155158
require.True(t, errors.IsNotFound(cl.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(obj), inCluster)))
156159
})
160+
t.Run("MigrateSSA", func(t *testing.T) {
161+
for _, setup := range []struct {
162+
defaultMigrate bool
163+
explicitMigrate *bool
164+
migrationExpected bool
165+
}{
166+
{
167+
defaultMigrate: false,
168+
explicitMigrate: ptr.To(true),
169+
migrationExpected: true,
170+
},
171+
{
172+
defaultMigrate: false,
173+
explicitMigrate: ptr.To(false),
174+
migrationExpected: false,
175+
},
176+
{
177+
defaultMigrate: false,
178+
explicitMigrate: nil,
179+
migrationExpected: false,
180+
},
181+
{
182+
defaultMigrate: true,
183+
explicitMigrate: ptr.To(true),
184+
migrationExpected: true,
185+
},
186+
{
187+
defaultMigrate: true,
188+
explicitMigrate: ptr.To(false),
189+
migrationExpected: false,
190+
},
191+
{
192+
defaultMigrate: true,
193+
explicitMigrate: nil,
194+
migrationExpected: true,
195+
},
196+
} {
197+
testName := fmt.Sprintf("default: %v, explicit: %v", setup.defaultMigrate, setup.explicitMigrate)
198+
t.Run(testName, func(t *testing.T) {
199+
// given
200+
obj := &corev1.Service{
201+
ObjectMeta: metav1.ObjectMeta{
202+
Name: "obj",
203+
Namespace: "default",
204+
ManagedFields: []metav1.ManagedFieldsEntry{
205+
{
206+
FieldsType: "FieldsV1",
207+
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec": {"f:selector": {}}}`)},
208+
Manager: strings.Split(rest.DefaultKubernetesUserAgent(), "/")[0],
209+
Operation: metav1.ManagedFieldsOperationUpdate,
210+
},
211+
},
212+
},
213+
Spec: corev1.ServiceSpec{},
214+
}
215+
toApply := obj.DeepCopy()
216+
toApply.SetManagedFields(nil)
217+
218+
cl, acl := NewTestSsaApplyClient(t, obj)
219+
acl.MigrateSSAByDefault = setup.defaultMigrate
220+
221+
// when
222+
var opts []client.SSAApplyObjectOption
223+
if setup.explicitMigrate != nil {
224+
opts = append(opts, client.MigrateSSA(*setup.explicitMigrate))
225+
}
226+
inCluster := &corev1.Service{}
227+
require.NoError(t, cl.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(obj), inCluster))
228+
require.NoError(t, acl.ApplyObject(context.TODO(), toApply, opts...))
229+
230+
// then
231+
inCluster = &corev1.Service{}
232+
require.NoError(t, cl.Get(context.TODO(), runtimeclient.ObjectKeyFromObject(obj), inCluster))
233+
if setup.migrationExpected {
234+
assert.Len(t, inCluster.ManagedFields, 1)
235+
assert.Equal(t, "test-field-owner", inCluster.ManagedFields[0].Manager)
236+
assert.Equal(t, metav1.ManagedFieldsOperationApply, inCluster.ManagedFields[0].Operation)
237+
} else {
238+
assert.Len(t, inCluster.ManagedFields, 1)
239+
assert.NotEqual(t, "test-field-owner", inCluster.ManagedFields[0].Manager)
240+
assert.Equal(t, metav1.ManagedFieldsOperationUpdate, inCluster.ManagedFields[0].Operation)
241+
}
242+
})
243+
}
244+
})
157245
t.Run("propagates k8s errors", func(t *testing.T) {
158246
// given
159247
cl, acl := NewTestSsaApplyClient(t)

0 commit comments

Comments
 (0)