diff --git a/pkg/action/install.go b/pkg/action/install.go index 6fc919366..813d65ba7 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -327,6 +327,29 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st } if !interactWithServer(i.DryRunStrategy) { + // Work on a local copy of the Configuration so client-only mocks + // (fake KubeClient, in-memory Releases, default Capabilities) do + // not mutate the shared *Configuration passed to NewInstall. + // Callers that reuse the same cfg for a later real install must + // still observe the original client and storage. See #11463. + // + // Copy exported fields only. Configuration has an unexported mutex and + // embeds logging.LogHolder (atomic.Pointer); both must not be copied + // by value (govet copylocks). + origCfg := i.cfg + cfgCopy := &Configuration{ + RESTClientGetter: origCfg.RESTClientGetter, + Releases: origCfg.Releases, + KubeClient: origCfg.KubeClient, + RegistryClient: origCfg.RegistryClient, + Capabilities: origCfg.Capabilities, + CustomTemplateFuncs: origCfg.CustomTemplateFuncs, + HookOutputFunc: origCfg.HookOutputFunc, + } + cfgCopy.SetLogger(origCfg.Logger().Handler()) + i.cfg = cfgCopy + defer func() { i.cfg = origCfg }() + // Add mock objects in here so it doesn't use Kube API server // NOTE(bacongobbler): used for `helm template` i.cfg.Capabilities = common.DefaultCapabilities.Copy() diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 2d83abe27..9be36b6ec 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -1253,3 +1253,39 @@ func TestInstallRelease_WaitOptionsPassedDownstream(t *testing.T) { // Verify that WaitOptions were passed to GetWaiter is.NotEmpty(failer.RecordedWaitOptions, "WaitOptions should be passed to GetWaiter") } + +// TestInstallDryRunClientDoesNotMutateSharedConfiguration ensures that a +// client-only install does not permanently replace the shared Configuration's +// KubeClient, Releases, or Capabilities (issue #11463). +func TestInstallDryRunClientDoesNotMutateSharedConfiguration(t *testing.T) { + req := require.New(t) + + config := actionConfigFixture(t) + originalKubeClient := config.KubeClient + originalCapabilities := config.Capabilities + originalReleases := config.Releases + + clientOnly := NewInstall(config) + clientOnly.DryRunStrategy = DryRunClient + clientOnly.ReleaseName = "test-client-only" + clientOnly.Namespace = "spaced" + + _, err := clientOnly.Run(buildChart(), nil) + req.NoError(err) + + req.Same(originalKubeClient, config.KubeClient, "KubeClient must not be replaced by client-only install") + req.Same(originalCapabilities, config.Capabilities, "Capabilities must not be replaced by client-only install") + req.Same(originalReleases, config.Releases, "Releases must not be replaced by client-only install") + + // A subsequent real install sharing the same Configuration must still work. + realInstall := NewInstall(config) + realInstall.DryRunStrategy = DryRunNone + realInstall.ReleaseName = "test-real-install" + realInstall.Namespace = "spaced" + + _, err = realInstall.Run(buildChart(), nil) + req.NoError(err) + + req.Same(originalKubeClient, config.KubeClient, "KubeClient must still be original after real install") + req.Same(originalReleases, config.Releases, "Releases must still be original after real install") +}