diff --git a/pkg/action/install.go b/pkg/action/install.go index 6fc919366..48b0d8bf8 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -221,10 +221,21 @@ func (i *Install) installCRDs(crds []chart.CRD) error { if len(totalItems) > 0 { var waiter kube.Waiter var err error + // CRD establishment must always be awaited, regardless of the wait + // strategy configured for the release. HookOnlyStrategy's Wait is a + // no-op by design so that it can skip waiting for general chart + // resources, but skipping it here too lets Helm start creating + // custom resources before their CRDs are recognized by the API + // server. Use StatusWatcherStrategy for this check instead so CRDs + // are still waited for, matching Helm 3's unconditional behavior. + crdWaitStrategy := i.WaitStrategy + if crdWaitStrategy == kube.HookOnlyStrategy { + crdWaitStrategy = kube.StatusWatcherStrategy + } if c, supportsOptions := i.cfg.KubeClient.(kube.InterfaceWaitOptions); supportsOptions { - waiter, err = c.GetWaiterWithOptions(i.WaitStrategy, i.WaitOptions...) + waiter, err = c.GetWaiterWithOptions(crdWaitStrategy, i.WaitOptions...) } else { - waiter, err = i.cfg.KubeClient.GetWaiter(i.WaitStrategy) + waiter, err = i.cfg.KubeClient.GetWaiter(crdWaitStrategy) } if err != nil { return fmt.Errorf("unable to get waiter: %w", err) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 2d83abe27..f323d047e 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -1182,6 +1182,28 @@ func TestInstallCRDs_WaiterError(t *testing.T) { require.Error(t, instAction.installCRDs(crdsToInstall), "wait error") } +// TestInstallCRDs_HookOnlyStrategyStillWaitsForEstablishment guards against +// https://github.com/helm/helm/issues/32671: HookOnlyStrategy's waiter does not +// wait on general resources, so installCRDs() must request a +// StatusWatcherStrategy waiter for CRD establishment instead. +func TestInstallCRDs_HookOnlyStrategyStillWaitsForEstablishment(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, BuildDummy: true} + config.KubeClient = &failingKubeClient + instAction := NewInstall(config) + instAction.WaitStrategy = kube.HookOnlyStrategy + + mockFile := common.File{ + Name: "crds/foo.yaml", + Data: []byte("hello"), + } + mockChart := buildChart(withFile(mockFile)) + crdsToInstall := mockChart.CRDObjects() + + require.NoError(t, instAction.installCRDs(crdsToInstall)) + assert.Equal(t, []kube.WaitStrategy{kube.StatusWatcherStrategy}, failingKubeClient.RecordedWaitStrategies) +} + func TestCheckDependencies(t *testing.T) { dependency := chart.Dependency{Name: "hello"} mockChart := buildChart(withDependency()) diff --git a/pkg/kube/fake/failing_kube_client.go b/pkg/kube/fake/failing_kube_client.go index 75d0c8de1..51fab26af 100644 --- a/pkg/kube/fake/failing_kube_client.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -50,7 +50,9 @@ type FailingKubeClient struct { WaitDuration time.Duration // RecordedWaitOptions stores the WaitOptions passed to GetWaiter for testing RecordedWaitOptions []kube.WaitOption - mu sync.Mutex + // RecordedWaitStrategies stores the WaitStrategy passed to each GetWaiter call for testing + RecordedWaitStrategies []kube.WaitStrategy + mu sync.Mutex } var _ kube.Interface = &FailingKubeClient{} @@ -160,14 +162,15 @@ func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) return f.GetWaiterWithOptions(ws) } -func (f *FailingKubeClient) appendRecordedWaitOptionsLocked(opts ...kube.WaitOption) { +func (f *FailingKubeClient) recordGetWaiterCallLocked(ws kube.WaitStrategy, opts ...kube.WaitOption) { f.mu.Lock() defer f.mu.Unlock() + f.RecordedWaitStrategies = append(f.RecordedWaitStrategies, ws) f.RecordedWaitOptions = append(f.RecordedWaitOptions, opts...) } func (f *FailingKubeClient) GetWaiterWithOptions(ws kube.WaitStrategy, opts ...kube.WaitOption) (kube.Waiter, error) { - f.appendRecordedWaitOptionsLocked(opts...) + f.recordGetWaiterCallLocked(ws, opts...) waiter, _ := f.PrintingKubeClient.GetWaiterWithOptions(ws, opts...) printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) return &FailingKubeWaiter{