diff --git a/pkg/action/install.go b/pkg/action/install.go index 89755d4e5..90c673b9c 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -488,7 +488,8 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource resources, kube.ClientUpdateOptionForceReplace(i.ForceReplace), kube.ClientUpdateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts), - kube.ClientUpdateOptionThreeWayMergeForUnstructured(updateThreeWayMergeForUnstructured)) + kube.ClientUpdateOptionThreeWayMergeForUnstructured(updateThreeWayMergeForUnstructured), + kube.ClientUpdateOptionUpgradeClientSideFieldManager(true)) } if err != nil { return rel, err diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 5f0ed02f1..adaf22615 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -210,7 +210,8 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas target, kube.ClientUpdateOptionForceReplace(r.ForceReplace), kube.ClientUpdateOptionServerSideApply(serverSideApply, r.ForceConflicts), - kube.ClientUpdateOptionThreeWayMergeForUnstructured(false)) + kube.ClientUpdateOptionThreeWayMergeForUnstructured(false), + kube.ClientUpdateOptionUpgradeClientSideFieldManager(true)) if err != nil { msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 41ddf859f..d86ac7752 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -399,6 +399,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR defer close(doneChan) go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease, serverSideApply) go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease) + select { case result := <-rChan: return result.r, result.e @@ -431,6 +432,11 @@ func (u *Upgrade) handleContext(ctx context.Context, done chan interface{}, c ch return } } + +func isReleaseApplyMethodClientSideApply(applyMethod string) bool { + return applyMethod == "" || applyMethod == string(release.ApplyMethodClientSideApply) +} + func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) { // pre-upgrade hooks @@ -443,11 +449,13 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name) } + upgradeClientSideFieldManager := isReleaseApplyMethodClientSideApply(originalRelease.ApplyMethod) && serverSideApply // Update client-side field manager if transitioning from client-side to server-side apply results, err := u.cfg.KubeClient.Update( current, target, kube.ClientUpdateOptionForceReplace(u.ForceReplace), - kube.ClientUpdateOptionServerSideApply(serverSideApply, u.ForceConflicts)) + kube.ClientUpdateOptionServerSideApply(serverSideApply, u.ForceConflicts), + kube.ClientUpdateOptionUpgradeClientSideFieldManager(upgradeClientSideFieldManager)) if err != nil { u.cfg.recordRelease(originalRelease) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 016055392..c41165490 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -47,12 +47,14 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/jsonmergepatch" "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + "k8s.io/client-go/util/csaupgrade" "k8s.io/client-go/util/retry" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -577,12 +579,13 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA } type clientUpdateOptions struct { - threeWayMergeForUnstructured bool - serverSideApply bool - forceReplace bool - forceConflicts bool - dryRun bool - fieldValidationDirective FieldValidationDirective + threeWayMergeForUnstructured bool + serverSideApply bool + forceReplace bool + forceConflicts bool + dryRun bool + fieldValidationDirective FieldValidationDirective + upgradeClientSideFieldManager bool } type ClientUpdateOption func(*clientUpdateOptions) error @@ -640,14 +643,32 @@ func ClientUpdateOptionDryRun(dryRun bool) ClientUpdateOption { // - For server-side apply: the directive is sent to the server to perform the validation // // Defaults to `FieldValidationDirectiveStrict` -func ClientUpdateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientCreateOption { - return func(o *clientCreateOptions) error { +func ClientUpdateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientUpdateOption { + return func(o *clientUpdateOptions) error { o.fieldValidationDirective = fieldValidationDirective return nil } } +// ClientUpdateOptionUpgradeClientSideFieldManager specifies that resources client-side field manager should be upgraded to server-side apply +// (before applying the object server-side) +// This is required when upgrading a chart from client-side to server-side apply, otherwise the client-side field management remains. Conflicting with server-side applied updates. +// +// Note: +// if this option is specified, but the object is not managed by client-side field manager, it will be a no-op. However, the cost of fetching the objects will be incurred. +// +// see: +// - https://github.com/kubernetes/kubernetes/pull/112905 +// - `UpgradeManagedFields` / https://github.com/kubernetes/kubernetes/blob/f47e9696d7237f1011d23c9b55f6947e60526179/staging/src/k8s.io/client-go/util/csaupgrade/upgrade.go#L81 +func ClientUpdateOptionUpgradeClientSideFieldManager(upgradeClientSideFieldManager bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.upgradeClientSideFieldManager = upgradeClientSideFieldManager + + return nil + } +} + type UpdateApplyFunc func(original, target *resource.Info) error // Update takes the current list of objects and target list of objects and @@ -707,15 +728,28 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate "using server-side apply for resource update", slog.Bool("forceConflicts", updateOptions.forceConflicts), slog.Bool("dryRun", updateOptions.dryRun), - slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective))) - return func(_, target *resource.Info) error { - err := patchResourceServerSide(target, updateOptions.dryRun, updateOptions.forceConflicts, updateOptions.fieldValidationDirective) + slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)), + slog.Bool("upgradeClientSideFieldManager", updateOptions.upgradeClientSideFieldManager)) + return func(original, target *resource.Info) error { logger := slog.With( slog.String("namespace", target.Namespace), slog.String("name", target.Name), slog.String("gvk", target.Mapping.GroupVersionKind.String())) - if err != nil { + + if updateOptions.upgradeClientSideFieldManager { + patched, err := upgradeClientSideFieldManager(original, updateOptions.dryRun, updateOptions.fieldValidationDirective) + if err != nil { + slog.Debug("Error patching resource to replace CSA field management", slog.Any("error", err)) + return err + } + + if patched { + logger.Debug("Upgraded object client-side field management with server-side apply field management") + } + } + + if err := patchResourceServerSide(target, updateOptions.dryRun, updateOptions.forceConflicts, updateOptions.fieldValidationDirective); err != nil { logger.Debug("Error patching resource", slog.Any("error", err)) return err } @@ -996,19 +1030,76 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr return nil } +// upgradeClientSideFieldManager is simply a wrapper around csaupgrade.UpgradeManagedFields +// that ugrade CSA managed fields to SSA apply +// see: https://github.com/kubernetes/kubernetes/pull/112905 +func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) { + + fieldManagerName := getManagedFieldsManager() + + patched := false + err := retry.RetryOnConflict( + retry.DefaultRetry, + func() error { + + if err := info.Get(); err != nil { + return fmt.Errorf("failed to get object %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) + } + + helper := resource.NewHelper( + info.Client, + info.Mapping). + DryRun(dryRun). + WithFieldManager(fieldManagerName). + WithFieldValidation(string(fieldValidationDirective)) + + patchData, err := csaupgrade.UpgradeManagedFieldsPatch( + info.Object, + sets.New(fieldManagerName), + fieldManagerName) + if err != nil { + return fmt.Errorf("failed to upgrade managed fields for object %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) + } + + if len(patchData) == 0 { + return nil + } + + obj, err := helper.Patch( + info.Namespace, + info.Name, + types.JSONPatchType, + patchData, + nil) + + if err == nil { + patched = true + return info.Refresh(obj, true) + } + + if !apierrors.IsConflict(err) { + return fmt.Errorf("failed to patch object to upgrade CSA field manager %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) + } + + return err + }) + + return patched, err +} + // Patch reource using server-side apply func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error { helper := resource.NewHelper( target.Client, target.Mapping). DryRun(dryRun). - WithFieldManager(ManagedFieldsManager). + WithFieldManager(getManagedFieldsManager()). WithFieldValidation(string(fieldValidationDirective)) // Send the full object to be applied on the server side. data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, target.Object) if err != nil { - return fmt.Errorf("failed to encode object %s/%s with kind %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.Kind, err) + return fmt.Errorf("failed to encode object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) } options := metav1.PatchOptions{ Force: &forceConflicts, @@ -1026,7 +1117,7 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts } if apierrors.IsConflict(err) { - return fmt.Errorf("conflict occurred while applying %s/%s with kind %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.Kind, err) + return fmt.Errorf("conflict occurred while applying object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) } return err diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 5060a5fc2..a8a8668c7 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -339,9 +339,11 @@ func TestUpdate(t *testing.T) { } expectedActionsServerSideApply := []string{ + "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:PATCH", "/namespaces/default/pods/otter:GET", + "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/otter:PATCH", "/namespaces/default/pods/dolphin:GET", "/namespaces/default/pods:POST", // create dolphin @@ -467,7 +469,8 @@ func TestUpdate(t *testing.T) { second, ClientUpdateOptionThreeWayMergeForUnstructured(tc.ThreeWayMergeForUnstructured), ClientUpdateOptionForceReplace(false), - ClientUpdateOptionServerSideApply(tc.ServerSideApply, false)) + ClientUpdateOptionServerSideApply(tc.ServerSideApply, false), + ClientUpdateOptionUpgradeClientSideFieldManager(true)) require.NoError(t, err) assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))