diff --git a/pkg/action/install.go b/pkg/action/install.go index ecf3ea340..09fc088d4 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -394,6 +394,34 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st // Bail out here if it is a dry run if isDryRun(i.DryRunStrategy) { + // For server-side dry-run, validate resources against the API server + if i.DryRunStrategy == DryRunServer { + var errs []error + if len(toBeAdopted) > 0 { + _, err := i.cfg.KubeClient.Update( + toBeAdopted, + resources, + kube.ClientUpdateOptionServerSideApply(i.ServerSideApply, i.ForceConflicts), + kube.ClientUpdateOptionDryRun(true), + ) + if err != nil { + errs = append(errs, err) + } + } + if len(resources) > 0 { + _, err := i.cfg.KubeClient.Create( + resources, + kube.ClientCreateOptionServerSideApply(i.ServerSideApply, false), + kube.ClientCreateOptionDryRun(true), + ) + if err != nil { + errs = append(errs, err) + } + } + if err := errors.Join(errs...); err != nil { + return rel, err + } + } rel.Info.Description = "Dry run complete" return rel, nil } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 374b318e1..800fb1218 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -412,6 +412,40 @@ func TestInstallRelease_DryRunClient(t *testing.T) { } } +func TestInstallRelease_DryRunServerValidation(t *testing.T) { + is := assert.New(t) + + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) + + instAction := NewInstall(config) + instAction.Namespace = "spaced" + instAction.ReleaseName = "test-server-dry-run" + + expectedErr := errors.New("validation error: unknown field in spec") + config.KubeClient.(*kubefake.FailingKubeClient).CreateError = expectedErr + instAction.DryRunStrategy = DryRunServer + + vals := map[string]interface{}{} + _, err := instAction.Run(buildChart(withSampleTemplates()), vals) + + is.Error(err) + is.Contains(err.Error(), "validation error") + + config2 := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) + config2.KubeClient.(*kubefake.FailingKubeClient).CreateError = expectedErr + + instAction2 := NewInstall(config2) + instAction2.Namespace = "spaced" + instAction2.ReleaseName = "test-client-dry-run" + instAction2.DryRunStrategy = DryRunClient + + resi, err := instAction2.Run(buildChart(withSampleTemplates()), vals) + is.NoError(err) + res, err := releaserToV1Release(resi) + is.NoError(err) + is.Equal(res.Info.Description, "Dry run complete") +} + func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 13d28fd4d..6226a49ce 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -392,6 +392,19 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR if isDryRun(u.DryRunStrategy) { u.cfg.Logger().Debug("dry run for release", "name", upgradedRelease.Name) + // For server-side dry-run, validate resources against the API server + if u.DryRunStrategy == DryRunServer { + _, err := u.cfg.KubeClient.Update( + current, + target, + kube.ClientUpdateOptionForceReplace(u.ForceReplace), + kube.ClientUpdateOptionServerSideApply(serverSideApply, u.ForceConflicts), + kube.ClientUpdateOptionDryRun(true), + ) + if err != nil { + return upgradedRelease, err + } + } if len(u.Description) > 0 { upgradedRelease.Info.Description = u.Description } else { diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index e1eac3f9f..f081690cf 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -635,6 +635,55 @@ func TestUpgradeRelease_DryRun(t *testing.T) { req.Error(err) } +func TestUpgradeRelease_DryRunServerValidation(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) + + upAction := NewUpgrade(config) + upAction.Namespace = "spaced" + + rel := releaseStub() + rel.Name = "test-server-dry-run" + rel.Info.Status = common.StatusDeployed + req.NoError(upAction.cfg.Releases.Create(rel)) + + expectedErr := errors.New("validation error: unknown field in spec") + config.KubeClient.(*kubefake.FailingKubeClient).UpdateError = expectedErr + upAction.DryRunStrategy = DryRunServer + + vals := map[string]interface{}{} + ctx, done := context.WithCancel(t.Context()) + _, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + done() + + is.Error(err) + is.Contains(err.Error(), "validation error") + + config2 := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) + config2.KubeClient.(*kubefake.FailingKubeClient).UpdateError = expectedErr + + upAction2 := NewUpgrade(config2) + upAction2.Namespace = "spaced" + + rel2 := releaseStub() + rel2.Name = "test-client-dry-run" + rel2.Info.Status = common.StatusDeployed + req.NoError(upAction2.cfg.Releases.Create(rel2)) + + upAction2.DryRunStrategy = DryRunClient + + ctx, done = context.WithCancel(t.Context()) + resi, err := upAction2.RunWithContext(ctx, rel2.Name, buildChart(), vals) + done() + + is.NoError(err) + res, err := releaserToV1Release(resi) + is.NoError(err) + is.Equal(res.Info.Description, "Dry run complete") +} + func TestGetUpgradeServerSideValue(t *testing.T) { tests := []struct { name string