From c364b639fb48e13a5b0925309b00226d3758999c Mon Sep 17 00:00:00 2001 From: Alexander Zielenski <351783+alexzielenski@users.noreply.github.com> Date: Mon, 22 Aug 2022 16:42:18 -0700 Subject: [PATCH] add function to upgrade managedfields CSA to SSA Kubernetes-commit: 27cd307e23df3a2d508d52ff10ac8f46bf3bcea3 --- util/csaupgrade/upgrade.go | 134 ++++++++++++++++++++++++++++++++ util/csaupgrade/upgrade_test.go | 121 ++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 util/csaupgrade/upgrade.go create mode 100644 util/csaupgrade/upgrade_test.go diff --git a/util/csaupgrade/upgrade.go b/util/csaupgrade/upgrade.go new file mode 100644 index 00000000..a4ea0879 --- /dev/null +++ b/util/csaupgrade/upgrade.go @@ -0,0 +1,134 @@ +package csaupgrade + +import ( + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" +) + +// Upgrades the Manager information for fields managed with CSA +// Prepares fields owned by `csaManager` for 'Update' operations for use now +// with the given `ssaManager` for `Apply` operations +// +// csaManager - Name of FieldManager formerly used for `Update` operations +// ssaManager - Name of FieldManager formerly used for `Apply` operations +// subResource - Name of subresource used for api calls or empty string for main resource +func UpgradeManagedFields( + obj runtime.Object, + csaManager string, + ssaManager string, + subResource string, +) (runtime.Object, error) { + accessor, err := meta.Accessor(obj) + if err != nil { + return nil, fmt.Errorf("error accessing object metadata: %w", err) + } + + managed, error := fieldmanager.DecodeManagedFields(accessor.GetManagedFields()) + if error != nil { + return nil, fmt.Errorf("failed to decode managed fields: %w", error) + } + // If SSA manager exists: + // find CSA manager of same version, union. discard the rest + // Else SSA manager does not exist: + // find most recent CSA manager. convert to Apply operation + + ssaIdentifier, err := fieldmanager.BuildManagerIdentifier(&metav1.ManagedFieldsEntry{ + Manager: ssaManager, + Operation: metav1.ManagedFieldsOperationApply, + Subresource: subResource, + }) + if err != nil { + return nil, fmt.Errorf("failed to build manager identifier for ssa manager") + } + + ssaMan, ssaExists := managed.Fields()[ssaIdentifier] + + // Collect all relevant CSA managers before operating on them + csaManagers := map[string]fieldpath.VersionedSet{} + for name, entry := range managed.Fields() { + if entry.Applied() { + // Not interested in SSA managed fields entries + continue + } + + // Manager string is a JSON representation of encoded entry + // Pull manager name and subresource from it + encodedVersionedSet := &metav1.ManagedFieldsEntry{} + err = json.Unmarshal([]byte(name), encodedVersionedSet) + if err != nil { + return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", name, err) + } + + if encodedVersionedSet.Manager != csaManager || + encodedVersionedSet.Subresource != subResource { + continue + } + + csaManagers[name] = entry + } + + if len(csaManagers) == 0 { + return obj, nil + } + + if ssaExists { + for name, entry := range csaManagers { + if entry.APIVersion() == ssaMan.APIVersion() { + // Merge entries if they are compatible versions + ssaMan = fieldpath.NewVersionedSet( + ssaMan.Set().Union(entry.Set()), + entry.APIVersion(), + true, + ) + managed.Fields()[ssaIdentifier] = ssaMan + } + + // Discard entry in all cases: + // if it has the wrong version we discard since managed fields versions + // cannot be converted + // if it has the correct version its fields were moved into the + // ssaManager's fieldSet + delete(managed.Fields(), name) + } + } else { + // Loop through sorted CSA managers. Take the first one we care about + firstName := "" + for _, entry := range accessor.GetManagedFields() { + if entry.Manager == csaManager && + entry.Subresource == subResource && + entry.Operation == metav1.ManagedFieldsOperationUpdate { + + if len(firstName) == 0 { + ident, err := fieldmanager.BuildManagerIdentifier(&entry) + if err != nil { + return nil, fmt.Errorf("failed to build manager identifier: %w", err) + } + + firstName = ident + break + } + } + } + + managed.Fields()[ssaIdentifier] = csaManagers[firstName] + + for name := range csaManagers { + delete(managed.Fields(), name) + } + } + + now := metav1.Now() + managed.Times()[ssaIdentifier] = &now + + copied := obj.DeepCopyObject() + if err := fieldmanager.EncodeObjectManagedFields(copied, managed); err != nil { + return nil, err + } + return copied, nil +} diff --git a/util/csaupgrade/upgrade_test.go b/util/csaupgrade/upgrade_test.go new file mode 100644 index 00000000..4113bfa7 --- /dev/null +++ b/util/csaupgrade/upgrade_test.go @@ -0,0 +1,121 @@ +package csaupgrade_test + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/util/csaupgrade" +) + +var avoidTimestampEqualities = func() conversion.Equalities { + var eqs = equality.Semantic.Copy() + + err := eqs.AddFunc( + func(a, b metav1.ManagedFieldsEntry) bool { + // Two objects' managed fields are equivalent if, ignoring timestamp, + // the objects are deeply equal. + a.Time = nil + b.Time = nil + return reflect.DeepEqual(a, b) + }, + ) + + if err != nil { + panic(err) + } + + return eqs +}() + +func TestUpgradeCSA(t *testing.T) { + // Initial object has managed fields from using CSA + originalYAML := []byte(` +apiVersion: v1 +data: + key: value + legacy: unused +kind: ConfigMap +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","data":{"key":"value","legacy":"unused"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"test","namespace":"default"}} + creationTimestamp: "2022-08-22T23:08:23Z" + managedFields: + - apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:data: + .: {} + f:key: {} + f:legacy: {} + f:metadata: + f:annotations: + .: {} + f:kubectl.kubernetes.io/last-applied-configuration: {} + manager: kubectl-client-side-apply + operation: Update + time: "2022-08-22T23:08:23Z" + name: test + namespace: default + resourceVersion: "12502" + uid: 1f186675-58e6-4d7b-8bc5-7ece581e3013 +`) + initialObject := unstructured.Unstructured{} + err := yaml.Unmarshal(originalYAML, &initialObject) + if err != nil { + t.Fatal(err) + } + + upgraded, err := csaupgrade.UpgradeManagedFields(&initialObject, "kubectl-client-side-apply", "kubectl", "") + if err != nil { + t.Fatal(err) + } + + expectedYAML := []byte(` +apiVersion: v1 +data: + key: value + legacy: unused +kind: ConfigMap +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"v1","data":{"key":"value","legacy":"unused"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"test","namespace":"default"}} + creationTimestamp: "2022-08-22T23:08:23Z" + managedFields: + - apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:data: + .: {} + f:key: {} + f:legacy: {} + f:metadata: + f:annotations: + .: {} + f:kubectl.kubernetes.io/last-applied-configuration: {} + manager: kubectl + operation: Apply + time: "2022-08-22T23:08:23Z" + name: test + namespace: default + resourceVersion: "12502" + uid: 1f186675-58e6-4d7b-8bc5-7ece581e3013 +`) + + expectedObject := unstructured.Unstructured{} + err = yaml.Unmarshal(expectedYAML, &expectedObject) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(&expectedObject, upgraded) { + t.Fatal(cmp.Diff(&expectedObject, upgraded)) + } +}