Skip to content

Commit 85bb4c3

Browse files
committed
Fix SSA and client apply migration cleanup
1 parent ca53881 commit 85bb4c3

4 files changed

Lines changed: 138 additions & 3 deletions

File tree

internal/remote/client.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,31 @@ func stripApplyHistoryAnnotations(obj model.K8sLocalObject) model.K8sLocalObject
690690
return ret
691691
}
692692

693+
func markLegacyApplyAnnotationsForDeletion(obj model.K8sLocalObject, remObj *unstructured.Unstructured) model.K8sLocalObject {
694+
if !hasLegacyClientSideApplyState(remObj) {
695+
return obj
696+
}
697+
ret := cloneLocalObject(obj)
698+
metadata, found, err := unstructured.NestedMap(ret.ToUnstructured().Object, "metadata")
699+
if err != nil || !found {
700+
metadata = map[string]interface{}{}
701+
}
702+
annotations, found, err := unstructured.NestedMap(metadata, "annotations")
703+
if err != nil || !found {
704+
annotations = map[string]interface{}{}
705+
}
706+
remoteAnnotations := remObj.GetAnnotations()
707+
if remoteAnnotations[model.QbecNames.PristineAnnotation] != "" {
708+
annotations[model.QbecNames.PristineAnnotation] = nil
709+
}
710+
if remoteAnnotations[kubectlLastConfig] != "" {
711+
annotations[kubectlLastConfig] = nil
712+
}
713+
metadata["annotations"] = annotations
714+
ret.ToUnstructured().Object["metadata"] = metadata
715+
return ret
716+
}
717+
693718
func managedFieldSet(obj *unstructured.Unstructured, fieldManager string) (*fieldpath.Set, error) {
694719
set := fieldpath.NewSet()
695720
if obj == nil {
@@ -872,8 +897,20 @@ func sameObject(lhs, rhs *unstructured.Unstructured, desired model.K8sLocalObjec
872897
), nil
873898
}
874899

900+
func hasLegacyClientSideApplyState(obj *unstructured.Unstructured) bool {
901+
if obj == nil {
902+
return false
903+
}
904+
annotations := obj.GetAnnotations()
905+
if len(annotations) == 0 {
906+
return false
907+
}
908+
return annotations[model.QbecNames.PristineAnnotation] != "" || annotations[kubectlLastConfig] != ""
909+
}
910+
875911
func (c *Client) serverSideApply(ctx context.Context, obj model.K8sLocalObject, remObj *unstructured.Unstructured, opts SyncOptions, operation string) (*updateResult, error) {
876912
obj = stripApplyHistoryAnnotations(obj)
913+
obj = markLegacyApplyAnnotationsForDeletion(obj, remObj)
877914
b, err := json.Marshal(obj)
878915
if err != nil {
879916
return nil, errors.Wrap(err, "json marshal")
@@ -897,7 +934,7 @@ func (c *Client) serverSideApply(ctx context.Context, obj model.K8sLocalObject,
897934
if opts.DryRun {
898935
patchOpts.DryRun = []string{metav1.DryRunAll}
899936
}
900-
if opts.ForceConflicts {
937+
if opts.ForceConflicts || hasLegacyClientSideApplyState(remObj) {
901938
force := true
902939
patchOpts.Force = &force
903940
}

internal/remote/client_ssa_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,40 @@ func TestMaybeUpdateServerSideApplyDetectsRemovedManagedFields(t *testing.T) {
350350
assert.Equal(t, apiTypes.ApplyPatchType, recorder.patchType)
351351
}
352352

353+
func TestMaybeUpdateServerSideApplyForcesLegacyClientSideMigration(t *testing.T) {
354+
existing := newConfigMap("default", "ssa-config").ToUnstructured()
355+
annotations := existing.GetAnnotations()
356+
annotations[model.QbecNames.PristineAnnotation] = "pristine"
357+
existing.SetAnnotations(annotations)
358+
359+
client, recorder := newServerSideApplyClient(t, existing.DeepCopy())
360+
result, err := client.maybeUpdate(context.Background(), newConfigMap("default", "ssa-config"), existing.DeepCopy(), SyncOptions{
361+
ApplyStrategy: model.ApplyStrategyServer,
362+
DisableUpdateFn: func(model.K8sMeta) bool { return false },
363+
}, internalSyncOptions{})
364+
require.NoError(t, err)
365+
assert.Equal(t, identicalObjects, result.SkipReason)
366+
require.NotNil(t, recorder.patchOptions.Force)
367+
assert.True(t, *recorder.patchOptions.Force)
368+
}
369+
370+
func TestMaybeUpdateServerSideApplyForcesKubectlMigration(t *testing.T) {
371+
existing := newConfigMap("default", "ssa-config").ToUnstructured()
372+
annotations := existing.GetAnnotations()
373+
annotations[kubectlLastConfig] = `{"apiVersion":"v1"}`
374+
existing.SetAnnotations(annotations)
375+
376+
client, recorder := newServerSideApplyClient(t, existing.DeepCopy())
377+
result, err := client.maybeUpdate(context.Background(), newConfigMap("default", "ssa-config"), existing.DeepCopy(), SyncOptions{
378+
ApplyStrategy: model.ApplyStrategyServer,
379+
DisableUpdateFn: func(model.K8sMeta) bool { return false },
380+
}, internalSyncOptions{})
381+
require.NoError(t, err)
382+
assert.Equal(t, identicalObjects, result.SkipReason)
383+
require.NotNil(t, recorder.patchOptions.Force)
384+
assert.True(t, *recorder.patchOptions.Force)
385+
}
386+
353387
func TestMaybeUpdateServerSideApplyTreatsEmptyDesiredCollectionAsUpdate(t *testing.T) {
354388
existing := newConfigMapWithData("default", "ssa-config", map[string]interface{}{
355389
"foo": "bar",

internal/remote/pristine.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"compress/gzip"
2020
"encoding/base64"
2121
"encoding/json"
22+
"strings"
2223

2324
"github.com/pkg/errors"
2425
"github.com/splunk/qbec/internal/model"
@@ -196,11 +197,42 @@ func pristineFromManagedFields(obj *unstructured.Unstructured, fieldManager stri
196197
return base, nil
197198
}
198199

199-
func pristineBytesForClientSideApply(obj *unstructured.Unstructured) ([]byte, error) {
200-
pristine, _ := getPristineVersion(obj, false)
200+
func mergeQbecMetadata(pristine, obj *unstructured.Unstructured) *unstructured.Unstructured {
201201
if pristine == nil {
202202
pristine = objectIdentityBase(obj)
203203
}
204+
205+
annotations := pristine.GetAnnotations()
206+
if annotations == nil {
207+
annotations = map[string]string{}
208+
}
209+
for k, v := range obj.GetAnnotations() {
210+
if strings.HasPrefix(k, model.QBECMetadataPrefix) || strings.HasPrefix(k, model.QBECDirectivesNamespace) {
211+
annotations[k] = v
212+
}
213+
}
214+
if len(annotations) > 0 {
215+
pristine.SetAnnotations(annotations)
216+
}
217+
218+
labels := pristine.GetLabels()
219+
if labels == nil {
220+
labels = map[string]string{}
221+
}
222+
for k, v := range obj.GetLabels() {
223+
if strings.HasPrefix(k, model.QBECMetadataPrefix) {
224+
labels[k] = v
225+
}
226+
}
227+
if len(labels) > 0 {
228+
pristine.SetLabels(labels)
229+
}
230+
return pristine
231+
}
232+
233+
func pristineBytesForClientSideApply(obj *unstructured.Unstructured) ([]byte, error) {
234+
pristine, _ := getPristineVersion(obj, false)
235+
pristine = mergeQbecMetadata(pristine, obj)
204236
return json.Marshal(pristine)
205237
}
206238

internal/remote/pristine_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,38 @@ func TestClientSideApplyUsesManagedFieldsPristineForMetadataDeletion(t *testing.
304304
assert.Contains(t, string(pristineBytes), `"qbec.io/component"`)
305305
}
306306

307+
func TestClientSideApplyUsesManagedFieldsPristineForDirectiveDeletion(t *testing.T) {
308+
desired := newConfigMap("default", "ssa-config")
309+
annotations := desired.ToUnstructured().GetAnnotations()
310+
delete(annotations, model.QbecNames.Directives.ApplyStrategy)
311+
desired.ToUnstructured().SetAnnotations(annotations)
312+
313+
serverObj := newConfigMap("default", "ssa-config").ToUnstructured()
314+
annotations = serverObj.GetAnnotations()
315+
annotations[model.QbecNames.Directives.ApplyStrategy] = string(model.ApplyStrategyServer)
316+
serverObj.SetAnnotations(annotations)
317+
serverObj.SetManagedFields([]metav1.ManagedFieldsEntry{
318+
{
319+
Manager: ssaFieldManager,
320+
Operation: metav1.ManagedFieldsOperationApply,
321+
FieldsType: "FieldsV1",
322+
FieldsV1: &metav1.FieldsV1{
323+
Raw: []byte(`{"f:data":{"f:foo":{}},"f:metadata":{"f:annotations":{"f:qbec.io~1component":{}},"f:labels":{"f:qbec.io~1application":{},"f:qbec.io~1environment":{}}}}`),
324+
},
325+
},
326+
})
327+
328+
p := patcher{cfgProvider: pristineBytesForClientSideApply}
329+
result, err := p.getPatchContents(serverObj, desired)
330+
require.NoError(t, err)
331+
assert.Empty(t, result.SkipReason)
332+
assert.Contains(t, string(result.patch), `"directives.qbec.io/apply-strategy":null`)
333+
334+
pristineBytes, err := pristineBytesForClientSideApply(serverObj)
335+
require.NoError(t, err)
336+
assert.Contains(t, string(pristineBytes), `"directives.qbec.io/apply-strategy":"server"`)
337+
}
338+
307339
func TestClientServerClientRoundTripNoop(t *testing.T) {
308340
desired := newConfigMap("default", "ssa-config")
309341
clientApplied, err := qbecPristine{}.createFromPristine(desired)

0 commit comments

Comments
 (0)