Helm client/SDK support server-side apply

Signed-off-by: George Jenkins <gvjenkins@gmail.com>
pull/30812/head
George Jenkins 4 months ago
parent 892a5cbfd0
commit e2dcbe28bf

@ -520,3 +520,10 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) io.Writer) { func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) io.Writer) {
cfg.HookOutputFunc = hookOutputFunc cfg.HookOutputFunc = hookOutputFunc
} }
func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod {
if serverSideApply {
return release.ApplyMethodServerSideApply
}
return release.ApplyMethodClientSideApply
}

@ -946,3 +946,8 @@ func TestRenderResources_NoPostRenderer(t *testing.T) {
assert.NotNil(t, buf) assert.NotNil(t, buf)
assert.Equal(t, "", notes) assert.Equal(t, "", notes)
} }
func TestDetermineReleaseSSAApplyMethod(t *testing.T) {
assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false))
assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true))
}

@ -47,6 +47,7 @@ type Metadata struct {
Revision int `json:"revision" yaml:"revision"` Revision int `json:"revision" yaml:"revision"`
Status string `json:"status" yaml:"status"` Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"` DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"`
} }
// NewGetMetadata creates a new GetMetadata object with the given configuration. // NewGetMetadata creates a new GetMetadata object with the given configuration.
@ -79,6 +80,7 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) {
Revision: rel.Version, Revision: rel.Version,
Status: rel.Info.Status.String(), Status: rel.Info.Status.String(),
DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339), DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339),
ApplyMethod: rel.ApplyMethod,
}, nil }, nil
} }

@ -33,7 +33,7 @@ import (
) )
// execHook executes all of the hooks for the given hook event. // execHook executes all of the hooks for the given hook event.
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, waitStrategy kube.WaitStrategy, timeout time.Duration) error { func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, waitStrategy kube.WaitStrategy, timeout time.Duration, serverSideApply bool) error {
executingHooks := []*release.Hook{} executingHooks := []*release.Hook{}
for _, h := range rl.Hooks { for _, h := range rl.Hooks {
@ -75,7 +75,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// Create hook resources // Create hook resources
if _, err := cfg.KubeClient.Create( if _, err := cfg.KubeClient.Create(
resources, resources,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil { kube.ClientCreateOptionServerSideApply(serverSideApply, false)); err != nil {
h.LastRun.CompletedAt = helmtime.Now() h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.Phase = release.HookPhaseFailed h.LastRun.Phase = release.HookPhaseFailed
return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err) return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err)

@ -385,7 +385,8 @@ data:
Capabilities: chartutil.DefaultCapabilities, Capabilities: chartutil.DefaultCapabilities,
} }
err := configuration.execHook(&tc.inputRelease, hookEvent, kube.StatusWatcherStrategy, 600) serverSideApply := true
err := configuration.execHook(&tc.inputRelease, hookEvent, kube.StatusWatcherStrategy, 600, serverSideApply)
if !reflect.DeepEqual(kubeClient.deleteRecord, tc.expectedDeleteRecord) { if !reflect.DeepEqual(kubeClient.deleteRecord, tc.expectedDeleteRecord) {
t.Fatalf("Got unexpected delete record, expected: %#v, but got: %#v", kubeClient.deleteRecord, tc.expectedDeleteRecord) t.Fatalf("Got unexpected delete record, expected: %#v, but got: %#v", kubeClient.deleteRecord, tc.expectedDeleteRecord)

@ -76,6 +76,12 @@ type Install struct {
// //
// This should be used with caution. // This should be used with caution.
ForceReplace bool ForceReplace bool
// ForceConflicts causes server-side apply to force conflicts ("Overwrite value, become sole manager")
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
ForceConflicts bool
// ServerSideApply when true (default) will enable changes to be applied via Kubernetes server-side apply
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply bool
CreateNamespace bool CreateNamespace bool
DryRun bool DryRun bool
DryRunOption string DryRunOption string
@ -146,6 +152,7 @@ type ChartPathOptions struct {
func NewInstall(cfg *Configuration) *Install { func NewInstall(cfg *Configuration) *Install {
in := &Install{ in := &Install{
cfg: cfg, cfg: cfg,
ServerSideApply: true,
} }
in.registryClient = cfg.RegistryClient in.registryClient = cfg.RegistryClient
@ -175,7 +182,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
// Send them to Kube // Send them to Kube
if _, err := i.cfg.KubeClient.Create( if _, err := i.cfg.KubeClient.Create(
res, res,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil { kube.ClientCreateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts)); err != nil {
// If the error is CRD already exists, continue. // If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) { if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name crdName := res[0].Name
@ -403,7 +410,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
} }
if _, err := i.cfg.KubeClient.Create( if _, err := i.cfg.KubeClient.Create(
resourceList, resourceList,
kube.ClientCreateOptionServerSideApply(false, false)); err != nil && !apierrors.IsAlreadyExists(err) { kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false)); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err return nil, err
} }
} }
@ -415,8 +422,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
} }
} }
// Store the release in history before continuing (new in Helm 3). We always know // Store the release in history before continuing. We always know that this is a create operation
// that this is a create operation.
if err := i.cfg.Releases.Create(rel); err != nil { if err := i.cfg.Releases.Create(rel); err != nil {
// We could try to recover gracefully here, but since nothing has been installed // We could try to recover gracefully here, but since nothing has been installed
// yet, this is probably safer than trying to continue when we know storage is // yet, this is probably safer than trying to continue when we know storage is
@ -463,7 +469,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
var err error var err error
// pre-install hooks // pre-install hooks
if !i.DisableHooks { if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.WaitStrategy, i.Timeout); err != nil { if err := i.cfg.execHook(rel, release.HookPreInstall, i.WaitStrategy, i.Timeout, i.ServerSideApply); err != nil {
return rel, fmt.Errorf("failed pre-install: %s", err) return rel, fmt.Errorf("failed pre-install: %s", err)
} }
} }
@ -474,15 +480,15 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
if len(toBeAdopted) == 0 && len(resources) > 0 { if len(toBeAdopted) == 0 && len(resources) > 0 {
_, err = i.cfg.KubeClient.Create( _, err = i.cfg.KubeClient.Create(
resources, resources,
kube.ClientCreateOptionServerSideApply(false, false)) kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false))
} else if len(resources) > 0 { } else if len(resources) > 0 {
updateThreeWayMergeForUnstructured := i.TakeOwnership useUpdateThreeWayMergeForUnstructured := i.TakeOwnership && !i.ServerSideApply // Use three-way merge when taking ownership (and not using server-side apply)
_, err = i.cfg.KubeClient.Update( _, err = i.cfg.KubeClient.Update(
toBeAdopted, toBeAdopted,
resources, resources,
kube.ClientUpdateOptionServerSideApply(false, false), kube.ClientUpdateOptionForceReplace(i.ForceReplace),
kube.ClientUpdateOptionThreeWayMergeForUnstructured(updateThreeWayMergeForUnstructured), kube.ClientUpdateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts),
kube.ClientUpdateOptionForceReplace(i.ForceReplace)) kube.ClientUpdateOptionThreeWayMergeForUnstructured(useUpdateThreeWayMergeForUnstructured))
} }
if err != nil { if err != nil {
return rel, err return rel, err
@ -503,7 +509,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource
} }
if !i.DisableHooks { if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.WaitStrategy, i.Timeout); err != nil { if err := i.cfg.execHook(rel, release.HookPostInstall, i.WaitStrategy, i.Timeout, i.ServerSideApply); err != nil {
return rel, fmt.Errorf("failed post-install: %s", err) return rel, fmt.Errorf("failed post-install: %s", err)
} }
} }
@ -580,7 +586,8 @@ func (i *Install) availableName() error {
// createRelease creates a new release object // createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release { func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
ts := i.cfg.Now() ts := i.cfg.Now()
return &release.Release{
r := &release.Release{
Name: i.ReleaseName, Name: i.ReleaseName,
Namespace: i.Namespace, Namespace: i.Namespace,
Chart: chrt, Chart: chrt,
@ -592,7 +599,10 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{
}, },
Version: 1, Version: 1,
Labels: labels, Labels: labels,
ApplyMethod: string(determineReleaseSSApplyMethod(i.ServerSideApply)),
} }
return r
} }
// recordRelease with an update operation in case reuse has been set. // recordRelease with an update operation in case reuse has been set.

@ -96,7 +96,8 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
rel.Hooks = executingHooks rel.Hooks = executingHooks
} }
if err := r.cfg.execHook(rel, release.HookTest, kube.StatusWatcherStrategy, r.Timeout); err != nil { serverSideApply := rel.ApplyMethod == string(release.ApplyMethodServerSideApply)
if err := r.cfg.execHook(rel, release.HookTest, kube.StatusWatcherStrategy, r.Timeout, serverSideApply); err != nil {
rel.Hooks = append(skippedHooks, rel.Hooks...) rel.Hooks = append(skippedHooks, rel.Hooks...)
r.cfg.Releases.Update(rel) r.cfg.Releases.Update(rel)
return rel, err return rel, err

@ -45,6 +45,14 @@ type Rollback struct {
// //
// This should be used with caution. // This should be used with caution.
ForceReplace bool ForceReplace bool
// ForceConflicts causes server-side apply to force conflicts ("Overwrite value, become sole manager")
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
ForceConflicts bool
// ServerSideApply enables changes to be applied via Kubernetes server-side apply
// Can be the string: "true", "false" or "auto"
// When "auto", sever-side usage will be based upon the releases previous usage
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply string
CleanupOnFail bool CleanupOnFail bool
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release MaxHistory int // MaxHistory limits the maximum number of revisions saved per release
} }
@ -65,7 +73,7 @@ func (r *Rollback) Run(name string) error {
r.cfg.Releases.MaxHistory = r.MaxHistory r.cfg.Releases.MaxHistory = r.MaxHistory
slog.Debug("preparing rollback", "name", name) slog.Debug("preparing rollback", "name", name)
currentRelease, targetRelease, err := r.prepareRollback(name) currentRelease, targetRelease, serverSideApply, err := r.prepareRollback(name)
if err != nil { if err != nil {
return err return err
} }
@ -78,7 +86,7 @@ func (r *Rollback) Run(name string) error {
} }
slog.Debug("performing rollback", "name", name) slog.Debug("performing rollback", "name", name)
if _, err := r.performRollback(currentRelease, targetRelease); err != nil { if _, err := r.performRollback(currentRelease, targetRelease, serverSideApply); err != nil {
return err return err
} }
@ -93,18 +101,18 @@ func (r *Rollback) Run(name string) error {
// prepareRollback finds the previous release and prepares a new release object with // prepareRollback finds the previous release and prepares a new release object with
// the previous release's configuration // the previous release's configuration
func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) { func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, bool, error) {
if err := chartutil.ValidateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, nil, fmt.Errorf("prepareRollback: Release name is invalid: %s", name) return nil, nil, false, fmt.Errorf("prepareRollback: Release name is invalid: %s", name)
} }
if r.Version < 0 { if r.Version < 0 {
return nil, nil, errInvalidRevision return nil, nil, false, errInvalidRevision
} }
currentRelease, err := r.cfg.Releases.Last(name) currentRelease, err := r.cfg.Releases.Last(name)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
previousVersion := r.Version previousVersion := r.Version
@ -114,7 +122,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
historyReleases, err := r.cfg.Releases.History(name) historyReleases, err := r.cfg.Releases.History(name)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
// Check if the history version to be rolled back exists // Check if the history version to be rolled back exists
@ -127,14 +135,19 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
} }
} }
if !previousVersionExist { if !previousVersionExist {
return nil, nil, fmt.Errorf("release has no %d version", previousVersion) return nil, nil, false, fmt.Errorf("release has no %d version", previousVersion)
} }
slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion) slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion)
previousRelease, err := r.cfg.Releases.Get(name, previousVersion) previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
}
serverSideApply, err := getUpgradeServerSideValue(r.ServerSideApply, previousRelease.ApplyMethod)
if err != nil {
return nil, nil, false, err
} }
// Store a new release object with previous release's configuration // Store a new release object with previous release's configuration
@ -156,12 +169,13 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Labels: previousRelease.Labels, Labels: previousRelease.Labels,
Manifest: previousRelease.Manifest, Manifest: previousRelease.Manifest,
Hooks: previousRelease.Hooks, Hooks: previousRelease.Hooks,
ApplyMethod: string(determineReleaseSSApplyMethod(serverSideApply)),
} }
return currentRelease, targetRelease, nil return currentRelease, targetRelease, serverSideApply, nil
} }
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) { func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) {
if r.DryRun { if r.DryRun {
slog.Debug("dry run", "name", targetRelease.Name) slog.Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil return targetRelease, nil
@ -177,15 +191,16 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
} }
// pre-rollback hooks // pre-rollback hooks
if !r.DisableHooks { if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.WaitStrategy, r.Timeout); err != nil { if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.WaitStrategy, r.Timeout, serverSideApply); err != nil {
return targetRelease, err return targetRelease, err
} }
} else { } else {
slog.Debug("rollback hooks disabled", "name", targetRelease.Name) slog.Debug("rollback hooks disabled", "name", targetRelease.Name)
} }
// It is safe to use "force" here because these are resources currently rendered by the chart. // It is safe to use "forceOwnership" here because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true)) err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true))
if err != nil { if err != nil {
return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err) return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err)
@ -193,8 +208,9 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
results, err := r.cfg.KubeClient.Update( results, err := r.cfg.KubeClient.Update(
current, current,
target, target,
kube.ClientUpdateOptionServerSideApply(false, false), kube.ClientUpdateOptionForceReplace(r.ForceReplace),
kube.ClientUpdateOptionForceReplace(r.ForceReplace)) kube.ClientUpdateOptionServerSideApply(serverSideApply, r.ForceConflicts),
kube.ClientUpdateOptionThreeWayMergeForUnstructured(false))
if err != nil { if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
@ -239,7 +255,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// post-rollback hooks // post-rollback hooks
if !r.DisableHooks { if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.WaitStrategy, r.Timeout); err != nil { if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.WaitStrategy, r.Timeout, serverSideApply); err != nil {
return targetRelease, err return targetRelease, err
} }
} }

@ -115,7 +115,8 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res := &release.UninstallReleaseResponse{Release: rel} res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.WaitStrategy, u.Timeout); err != nil { serverSideApply := true
if err := u.cfg.execHook(rel, release.HookPreDelete, u.WaitStrategy, u.Timeout, serverSideApply); err != nil {
return res, err return res, err
} }
} else { } else {
@ -144,7 +145,8 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
} }
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.WaitStrategy, u.Timeout); err != nil { serverSideApply := true
if err := u.cfg.execHook(rel, release.HookPostDelete, u.WaitStrategy, u.Timeout, serverSideApply); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} }
@ -243,6 +245,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
if err != nil { if err != nil {
return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)} return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)}
} }
if len(resources) > 0 {
if len(resources) > 0 { if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok { if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.DeletionPropagation)) _, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.DeletionPropagation))
@ -250,6 +253,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
} }
_, errs = u.cfg.KubeClient.Delete(resources) _, errs = u.cfg.KubeClient.Delete(resources)
} }
}
return resources, kept, errs return resources, kept, errs
} }

@ -21,6 +21,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake" kubefake "helm.sh/helm/v4/pkg/kube/fake"
@ -147,6 +148,6 @@ func TestUninstallRelease_Cascade(t *testing.T) {
failer.BuildDummy = true failer.BuildDummy = true
unAction.cfg.KubeClient = failer unAction.cfg.KubeClient = failer
_, err := unAction.Run(rel.Name) _, err := unAction.Run(rel.Name)
is.Error(err) require.Error(t, err)
is.Contains(err.Error(), "failed to delete release: come-fail-away") is.Contains(err.Error(), "failed to delete release: come-fail-away")
} }

@ -81,6 +81,14 @@ type Upgrade struct {
// //
// This should be used with caution. // This should be used with caution.
ForceReplace bool ForceReplace bool
// ForceConflicts causes server-side apply to force conflicts ("Overwrite value, become sole manager")
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
ForceConflicts bool
// ServerSideApply enables changes to be applied via Kubernetes server-side apply
// Can be the string: "true", "false" or "auto"
// When "auto", sever-side usage will be based upon the releases previous usage
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply string
// ResetValues will reset the values to the chart's built-ins rather than merging with existing. // ResetValues will reset the values to the chart's built-ins rather than merging with existing.
ResetValues bool ResetValues bool
// ReuseValues will reuse the user's last supplied values. // ReuseValues will reuse the user's last supplied values.
@ -128,6 +136,7 @@ type resultMessage struct {
func NewUpgrade(cfg *Configuration) *Upgrade { func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{ up := &Upgrade{
cfg: cfg, cfg: cfg,
ServerSideApply: "auto",
} }
up.registryClient = cfg.RegistryClient up.registryClient = cfg.RegistryClient
@ -162,7 +171,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
} }
slog.Debug("preparing upgrade", "name", name) slog.Debug("preparing upgrade", "name", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals) currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chart, vals)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,7 +179,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
u.cfg.Releases.MaxHistory = u.MaxHistory u.cfg.Releases.MaxHistory = u.MaxHistory
slog.Debug("performing update", "name", name) slog.Debug("performing update", "name", name)
res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease) res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease, serverSideApply)
if err != nil { if err != nil {
return res, err return res, err
} }
@ -195,14 +204,14 @@ func (u *Upgrade) isDryRun() bool {
} }
// prepareUpgrade builds an upgraded release for an upgrade operation. // prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) { func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
if chart == nil { if chart == nil {
return nil, nil, errMissingChart return nil, nil, false, errMissingChart
} }
// HideSecret must be used with dry run. Otherwise, return an error. // HideSecret must be used with dry run. Otherwise, return an error.
if !u.isDryRun() && u.HideSecret { if !u.isDryRun() && u.HideSecret {
return nil, nil, errors.New("hiding Kubernetes secrets requires a dry-run mode") return nil, nil, false, errors.New("hiding Kubernetes secrets requires a dry-run mode")
} }
// finds the last non-deleted release with the given name // finds the last non-deleted release with the given name
@ -210,14 +219,14 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
if err != nil { if err != nil {
// to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist // to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist
if errors.Is(err, driver.ErrReleaseNotFound) { if errors.Is(err, driver.ErrReleaseNotFound) {
return nil, nil, driver.NewErrNoDeployedReleases(name) return nil, nil, false, driver.NewErrNoDeployedReleases(name)
} }
return nil, nil, err return nil, nil, false, err
} }
// Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock. // Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock.
if lastRelease.Info.Status.IsPending() { if lastRelease.Info.Status.IsPending() {
return nil, nil, errPending return nil, nil, false, errPending
} }
var currentRelease *release.Release var currentRelease *release.Release
@ -232,7 +241,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
(lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) { (lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) {
currentRelease = lastRelease currentRelease = lastRelease
} else { } else {
return nil, nil, err return nil, nil, false, err
} }
} }
} }
@ -240,11 +249,11 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
// determine if values will be reused // determine if values will be reused
vals, err = u.reuseValues(chart, currentRelease, vals) vals, err = u.reuseValues(chart, currentRelease, vals)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
if err := chartutil.ProcessDependencies(chart, vals); err != nil { if err := chartutil.ProcessDependencies(chart, vals); err != nil {
return nil, nil, err return nil, nil, false, err
} }
// Increment revision count. This is passed to templates, and also stored on // Increment revision count. This is passed to templates, and also stored on
@ -260,11 +269,11 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
caps, err := u.cfg.getCapabilities() caps, err := u.cfg.getCapabilities()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation) valuesToRender, err := chartutil.ToRenderValuesWithSchemaValidation(chart, vals, options, caps, u.SkipSchemaValidation)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
// Determine whether or not to interact with remote // Determine whether or not to interact with remote
@ -275,13 +284,20 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret) hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, false, err
} }
if driver.ContainsSystemLabels(u.Labels) { if driver.ContainsSystemLabels(u.Labels) {
return nil, nil, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) return nil, nil, false, fmt.Errorf("user supplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
} }
serverSideApply, err := getUpgradeServerSideValue(u.ServerSideApply, lastRelease.ApplyMethod)
if err != nil {
return nil, nil, false, err
}
slog.Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod))
// Store an upgraded release. // Store an upgraded release.
upgradedRelease := &release.Release{ upgradedRelease := &release.Release{
Name: name, Name: name,
@ -298,16 +314,17 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
Manifest: manifestDoc.String(), Manifest: manifestDoc.String(),
Hooks: hooks, Hooks: hooks,
Labels: mergeCustomLabels(lastRelease.Labels, u.Labels), Labels: mergeCustomLabels(lastRelease.Labels, u.Labels),
ApplyMethod: string(determineReleaseSSApplyMethod(serverSideApply)),
} }
if len(notesTxt) > 0 { if len(notesTxt) > 0 {
upgradedRelease.Info.Notes = notesTxt upgradedRelease.Info.Notes = notesTxt
} }
err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation) err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation)
return currentRelease, upgradedRelease, err return currentRelease, upgradedRelease, serverSideApply, err
} }
func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) { func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release, serverSideApply bool) (*release.Release, error) {
current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false) current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false)
if err != nil { if err != nil {
// Checking for removed Kubernetes API error so can provide a more informative error message to the user // Checking for removed Kubernetes API error so can provide a more informative error message to the user
@ -380,7 +397,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
ctxChan := make(chan resultMessage) ctxChan := make(chan resultMessage)
doneChan := make(chan interface{}) doneChan := make(chan interface{})
defer close(doneChan) defer close(doneChan)
go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease) go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease, serverSideApply)
go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease) go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease)
select { select {
case result := <-rChan: case result := <-rChan:
@ -414,11 +431,11 @@ func (u *Upgrade) handleContext(ctx context.Context, done chan interface{}, c ch
return return
} }
} }
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) {
// pre-upgrade hooks // pre-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.WaitStrategy, u.Timeout); err != nil { if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.WaitStrategy, u.Timeout, serverSideApply); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
return return
} }
@ -429,8 +446,8 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
results, err := u.cfg.KubeClient.Update( results, err := u.cfg.KubeClient.Update(
current, current,
target, target,
kube.ClientUpdateOptionServerSideApply(false, false), kube.ClientUpdateOptionForceReplace(u.ForceReplace),
kube.ClientUpdateOptionForceReplace(u.ForceReplace)) kube.ClientUpdateOptionServerSideApply(serverSideApply, u.ForceConflicts))
if err != nil { if err != nil {
u.cfg.recordRelease(originalRelease) u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
@ -459,7 +476,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
// post-upgrade hooks // post-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.WaitStrategy, u.Timeout); err != nil { if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.WaitStrategy, u.Timeout, serverSideApply); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
return return
} }
@ -530,6 +547,8 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e
rollin.WaitForJobs = u.WaitForJobs rollin.WaitForJobs = u.WaitForJobs
rollin.DisableHooks = u.DisableHooks rollin.DisableHooks = u.DisableHooks
rollin.ForceReplace = u.ForceReplace rollin.ForceReplace = u.ForceReplace
rollin.ForceConflicts = u.ForceConflicts
rollin.ServerSideApply = u.ServerSideApply
rollin.Timeout = u.Timeout rollin.Timeout = u.Timeout
if rollErr := rollin.Run(rel.Name); rollErr != nil { if rollErr := rollin.Run(rel.Name); rollErr != nil {
return rel, fmt.Errorf("an error occurred while rolling back the release. original upgrade error: %w: %w", err, rollErr) return rel, fmt.Errorf("an error occurred while rolling back the release. original upgrade error: %w: %w", err, rollErr)
@ -607,3 +626,16 @@ func mergeCustomLabels(current, desired map[string]string) map[string]string {
} }
return labels return labels
} }
func getUpgradeServerSideValue(serverSideOption string, releaseApplyMethod string) (bool, error) {
switch serverSideOption {
case "auto":
return releaseApplyMethod == "ssa", nil
case "false":
return false, nil
case "true":
return true, nil
default:
return false, fmt.Errorf("invalid/unknown release server-side apply method: %s", serverSideOption)
}
}

@ -583,3 +583,109 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
done() done()
req.Error(err) req.Error(err)
} }
func TestGetUpgradeServerSideValue(t *testing.T) {
tests := []struct {
name string
actionServerSideOption string
releaseApplyMethod string
expectedServerSideApply bool
}{
{
name: "action ssa auto / release csa",
actionServerSideOption: "auto",
releaseApplyMethod: "csa",
expectedServerSideApply: false,
},
{
name: "action ssa auto / release ssa",
actionServerSideOption: "auto",
releaseApplyMethod: "ssa",
expectedServerSideApply: true,
},
{
name: "action ssa auto / release empty",
actionServerSideOption: "auto",
releaseApplyMethod: "",
expectedServerSideApply: false,
},
{
name: "action ssa true / release csa",
actionServerSideOption: "true",
releaseApplyMethod: "csa",
expectedServerSideApply: true,
},
{
name: "action ssa true / release ssa",
actionServerSideOption: "true",
releaseApplyMethod: "ssa",
expectedServerSideApply: true,
},
{
name: "action ssa true / release 'unknown'",
actionServerSideOption: "true",
releaseApplyMethod: "foo",
expectedServerSideApply: true,
},
{
name: "action ssa true / release empty",
actionServerSideOption: "true",
releaseApplyMethod: "",
expectedServerSideApply: true,
},
{
name: "action ssa false / release csa",
actionServerSideOption: "false",
releaseApplyMethod: "ssa",
expectedServerSideApply: false,
},
{
name: "action ssa false / release ssa",
actionServerSideOption: "false",
releaseApplyMethod: "ssa",
expectedServerSideApply: false,
},
{
name: "action ssa false / release 'unknown'",
actionServerSideOption: "false",
releaseApplyMethod: "foo",
expectedServerSideApply: false,
},
{
name: "action ssa false / release empty",
actionServerSideOption: "false",
releaseApplyMethod: "ssa",
expectedServerSideApply: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
serverSideApply, err := getUpgradeServerSideValue(tt.actionServerSideOption, tt.releaseApplyMethod)
assert.Nil(t, err)
assert.Equal(t, tt.expectedServerSideApply, serverSideApply)
})
}
testsError := []struct {
name string
actionServerSideOption string
releaseApplyMethod string
expectedErrorMsg string
}{
{
name: "action invalid option",
actionServerSideOption: "invalid",
releaseApplyMethod: "ssa",
expectedErrorMsg: "invalid/unknown release server-side apply method: invalid",
},
}
for _, tt := range testsError {
t.Run(tt.name, func(t *testing.T) {
_, err := getUpgradeServerSideValue(tt.actionServerSideOption, tt.releaseApplyMethod)
assert.ErrorContains(t, err, tt.expectedErrorMsg)
})
}
}

@ -27,6 +27,8 @@ import (
"helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
release "helm.sh/helm/v4/pkg/release/v1"
) )
type metadataWriter struct { type metadataWriter struct {
@ -75,6 +77,20 @@ func newGetMetadataCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
} }
func (w metadataWriter) WriteTable(out io.Writer) error { func (w metadataWriter) WriteTable(out io.Writer) error {
formatApplyMethod := func(applyMethod string) string {
switch applyMethod {
case "":
return "client-side apply (defaulted)"
case string(release.ApplyMethodClientSideApply):
return "client-side apply"
case string(release.ApplyMethodServerSideApply):
return "server-side apply"
default:
return fmt.Sprintf("unknown (%q)", applyMethod)
}
}
_, _ = fmt.Fprintf(out, "NAME: %v\n", w.metadata.Name) _, _ = fmt.Fprintf(out, "NAME: %v\n", w.metadata.Name)
_, _ = fmt.Fprintf(out, "CHART: %v\n", w.metadata.Chart) _, _ = fmt.Fprintf(out, "CHART: %v\n", w.metadata.Chart)
_, _ = fmt.Fprintf(out, "VERSION: %v\n", w.metadata.Version) _, _ = fmt.Fprintf(out, "VERSION: %v\n", w.metadata.Version)
@ -86,6 +102,7 @@ func (w metadataWriter) WriteTable(out io.Writer) error {
_, _ = fmt.Fprintf(out, "REVISION: %v\n", w.metadata.Revision) _, _ = fmt.Fprintf(out, "REVISION: %v\n", w.metadata.Revision)
_, _ = fmt.Fprintf(out, "STATUS: %v\n", w.metadata.Status) _, _ = fmt.Fprintf(out, "STATUS: %v\n", w.metadata.Status)
_, _ = fmt.Fprintf(out, "DEPLOYED_AT: %v\n", w.metadata.DeployedAt) _, _ = fmt.Fprintf(out, "DEPLOYED_AT: %v\n", w.metadata.DeployedAt)
_, _ = fmt.Fprintf(out, "APPLY_METHOD: %v\n", formatApplyMethod(w.metadata.ApplyMethod))
return nil return nil
} }

@ -196,6 +196,8 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") f.MarkDeprecated("force", "use --force-replace instead")
f.BoolVar(&client.ForceConflicts, "force-conflicts", false, "if set server-side apply will force changes against conflicts")
f.BoolVar(&client.ServerSideApply, "server-side", true, "object updates run in the server instead of the client")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "reuse the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.BoolVar(&client.Replace, "replace", false, "reuse the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
@ -217,6 +219,8 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2 requiredArgs := 2

@ -80,12 +80,16 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") f.MarkDeprecated("force", "use --force-replace instead")
f.BoolVar(&client.ForceConflicts, "force-conflicts", false, "if set server-side apply will force changes against conflicts")
f.StringVar(&client.ServerSideApply, "server-side", "auto", "must be \"true\", \"false\" or \"auto\". Object updates run in the server instead of the client (\"auto\" defaults the value from the previous chart release's method)")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")
f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
return cmd return cmd
} }

@ -9,3 +9,4 @@ NAMESPACE: default
REVISION: 1 REVISION: 1
STATUS: deployed STATUS: deployed
DEPLOYED_AT: 1977-09-02T22:04:05Z DEPLOYED_AT: 1977-09-02T22:04:05Z
APPLY_METHOD: client-side apply (defaulted)

@ -273,6 +273,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead") f.MarkDeprecated("force", "use --force-replace instead")
f.BoolVar(&client.ForceConflicts, "force-conflicts", false, "if set server-side apply will force changes against conflicts")
f.StringVar(&client.ServerSideApply, "server-side", "auto", "must be \"true\", \"false\" or \"auto\". Object updates run in the server instead of the client (\"auto\" defaults the value from the previous chart release's method)")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled")
@ -297,6 +299,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer) bindPostRenderFlag(cmd, &client.PostRenderer)
AddWaitFlag(cmd, &client.WaitStrategy) AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")
err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 2 { if len(args) != 2 {

@ -108,6 +108,14 @@ func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, [
return f.PrintingKubeClient.Delete(resources) return f.PrintingKubeClient.Delete(resources)
} }
// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}
// WatchUntilReady returns the configured error if set or prints // WatchUntilReady returns the configured error if set or prints
func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time.Duration) error { func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time.Duration) error {
if f.watchUntilReadyError != nil { if f.watchUntilReadyError != nil {
@ -146,14 +154,6 @@ func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList,
return f.PrintingKubeClient.BuildTable(r, false) return f.PrintingKubeClient.BuildTable(r, false)
} }
// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}
func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) { func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) {
waiter, _ := f.PrintingKubeClient.GetWaiter(ws) waiter, _ := f.PrintingKubeClient.GetWaiter(ws)
printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter)

@ -97,6 +97,17 @@ func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result,
return &kube.Result{Deleted: resources}, nil return &kube.Result{Deleted: resources}, nil
} }
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
// Update implements KubeClient Update. // Update implements KubeClient Update.
func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ ...kube.ClientUpdateOption) (*kube.Result, error) { func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ ...kube.ClientUpdateOption) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(modified)) _, err := io.Copy(p.Out, bufferize(modified))
@ -135,17 +146,6 @@ func (p *PrintingKubeClient) OutputContainerLogsForPodList(_ *v1.PodList, someNa
return err return err
} }
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
func (p *PrintingKubeClient) GetWaiter(_ kube.WaitStrategy) (kube.Waiter, error) { func (p *PrintingKubeClient) GetWaiter(_ kube.WaitStrategy) (kube.Waiter, error) {
return &PrintingKubeWaiter{Out: p.Out, LogOutput: p.LogOutput}, nil return &PrintingKubeWaiter{Out: p.Out, LogOutput: p.LogOutput}, nil
} }

@ -19,6 +19,11 @@ import (
chart "helm.sh/helm/v4/pkg/chart/v2" chart "helm.sh/helm/v4/pkg/chart/v2"
) )
type ApplyMethod string
const ApplyMethodClientSideApply ApplyMethod = "csa"
const ApplyMethodServerSideApply ApplyMethod = "ssa"
// Release describes a deployment of a chart, together with the chart // Release describes a deployment of a chart, together with the chart
// and the variables used to deploy that chart. // and the variables used to deploy that chart.
type Release struct { type Release struct {
@ -42,6 +47,9 @@ type Release struct {
// Labels of the release. // Labels of the release.
// Disabled encoding into Json cause labels are stored in storage driver metadata field. // Disabled encoding into Json cause labels are stored in storage driver metadata field.
Labels map[string]string `json:"-"` Labels map[string]string `json:"-"`
// ApplyMethod stores whether server-side or client-side apply was used for the release
// Unset (empty string) should be treated as the default of client-side apply
ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa"
} }
// SetStatus is a helper for setting the status on a release. // SetStatus is a helper for setting the status on a release.

Loading…
Cancel
Save