From fc74964f8a039ce209966b70fa7ba0fc7ea36a9e Mon Sep 17 00:00:00 2001 From: MichaelMorris Date: Fri, 3 Feb 2023 14:02:12 +0000 Subject: [PATCH 01/18] Add robustness to wait status checks When a user specifies --wait for an install or upgrade this commit will enable the user to specifiy a number of retries to attepmt if a status check fails Errors including a HTTP status code < 500 will not be retried Signed-off-by: MichaelMorris --- cmd/helm/install.go | 1 + cmd/helm/upgrade.go | 2 ++ pkg/action/install.go | 20 ++++++++++++----- pkg/action/install_test.go | 16 +++++++++++++ pkg/action/upgrade.go | 24 +++++++++++++------- pkg/action/upgrade_test.go | 46 ++++++++++++++++++++++++++++++++++++++ pkg/kube/client.go | 18 +++++++++++++-- pkg/kube/fake/fake.go | 17 ++++++++++++++ pkg/kube/fake/printer.go | 10 +++++++++ pkg/kube/interface.go | 12 ++++++++++ pkg/kube/wait.go | 45 ++++++++++++++++++++++++++++++++----- 11 files changed, 189 insertions(+), 22 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 6ffc968ce..ae68be121 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -167,6 +167,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state 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.IntVar(&client.WaitRetries, "wait-retries", 0, "if set and --wait enabled, will retry any failed check on resource state subject to the specified number of retries") f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&client.Description, "description", "", "add a custom description") diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 145d342b7..8b7634619 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -117,6 +117,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Timeout = client.Timeout instClient.Wait = client.Wait instClient.WaitForJobs = client.WaitForJobs + instClient.WaitRetries = client.WaitRetries instClient.Devel = client.Devel instClient.Namespace = client.Namespace instClient.Atomic = client.Atomic @@ -233,6 +234,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state 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.IntVar(&client.WaitRetries, "wait-retries", 0, "if set and --wait enabled, will retry any failed check on resource state, except if HTTP status code < 500 is received, subject to the specified number of retries") f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically if --atomic is used") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") diff --git a/pkg/action/install.go b/pkg/action/install.go index 5fb7e6736..63abc7dbd 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -77,6 +77,7 @@ type Install struct { Replace bool Wait bool WaitForJobs bool + WaitRetries int Devel bool DependencyUpdate bool Timeout time.Duration @@ -413,17 +414,24 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t } if i.Wait { + var err error if i.WaitForJobs { - if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { - i.reportToRun(c, rel, err) - return + if kubeClient, ok := i.cfg.KubeClient.(kube.InterfaceWithRetry); ok { + err = kubeClient.WaitWithJobsWithRetry(resources, i.Timeout, i.WaitRetries) + } else { + err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) } } else { - if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { - i.reportToRun(c, rel, err) - return + if kubeClient, ok := i.cfg.KubeClient.(kube.InterfaceWithRetry); ok { + err = kubeClient.WaitWithRetry(resources, i.Timeout, i.WaitRetries) + } else { + err = i.cfg.KubeClient.Wait(resources, i.Timeout) } } + if err != nil { + i.reportToRun(c, rel, err) + return + } } if !i.DisableHooks { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index dd0cdb54d..438f12439 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -393,6 +393,22 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled") is.Equal(res.Info.Status, release.StatusFailed) } +func TestInstallRelease_Wait_With_Retries(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "come-fail-away" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Wait = true + instAction.WaitRetries = 2 + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 784df4013..a35fe90ff 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -68,6 +68,8 @@ type Upgrade struct { Wait bool // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. WaitForJobs bool + // WaitRetries determines whether any failed resource state checks will be retried during the wait operation. + WaitRetries int // DisableHooks disables hook processing if set to true. DisableHooks bool // DryRun controls whether the operation is prepared, but not executed. @@ -402,19 +404,25 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele u.cfg.Log( "waiting for release %s resources (created: %d updated: %d deleted: %d)", upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted)) + var err error if u.WaitForJobs { - if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { - u.cfg.recordRelease(originalRelease) - u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) - return + if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceWithRetry); ok { + err = kubeClient.WaitWithJobsWithRetry(target, u.Timeout, u.WaitRetries) + } else { + err = u.cfg.KubeClient.WaitWithJobs(target, u.Timeout) } } else { - if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { - u.cfg.recordRelease(originalRelease) - u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) - return + if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceWithRetry); ok { + err = kubeClient.WaitWithRetry(target, u.Timeout, u.WaitRetries) + } else { + err = u.cfg.KubeClient.Wait(target, u.Timeout) } } + if err != nil { + u.cfg.recordRelease(originalRelease) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return + } } // post-upgrade hooks diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 62922b373..62e4fa411 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -88,6 +88,28 @@ func TestUpgradeRelease_Wait(t *testing.T) { is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) } +func TestUpgradeRelease_Wait_With_Retries(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.WaitRetries = 2 + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} func TestUpgradeRelease_WaitForJobs(t *testing.T) { is := assert.New(t) @@ -112,6 +134,30 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { is.Equal(res.Info.Status, release.StatusFailed) } +func TestUpgradeRelease_WaitForJobs_With_Retries(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.WaitForJobs = true + upAction.WaitRetries = 2 + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + func TestUpgradeRelease_CleanupOnFail(t *testing.T) { is := assert.New(t) req := require.New(t) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 7b3c803f9..a2c1a7a40 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -275,6 +275,13 @@ func getResource(info *resource.Info) (runtime.Object, error) { // Wait waits up to the given timeout for the specified resources to be ready. func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { + return c.WaitWithRetry(resources, timeout, 0) +} + +// WaitWithRetry waits up to the given timeout for the specified resources to be ready. If an error +// is encountered when checking on the status of a resource then retries will be performed subject +// to the given maximum number of retries +func (c *Client) WaitWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error { cs, err := c.getKubeClient() if err != nil { return err @@ -285,11 +292,18 @@ func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { log: c.Log, timeout: timeout, } - return w.waitForResources(resources) + return w.waitForResources(resources, waitRetries) } // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) error { + return c.WaitWithJobsWithRetry(resources, timeout, 0) +} + +// WaitWithJobsWithRetry waits up to the given timeout for the specified resources to be ready, including jobs. +// If an error is encountered when checking on the status of a resource then retries will be performed subject +// to the given maximum number of retries +func (c *Client) WaitWithJobsWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error { cs, err := c.getKubeClient() if err != nil { return err @@ -300,7 +314,7 @@ func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) err log: c.Log, timeout: timeout, } - return w.waitForResources(resources) + return w.waitForResources(resources, waitRetries) } // WaitForDelete wait up to the given timeout for the specified resources to be deleted. diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index 267020d57..4b351fa70 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -74,6 +74,15 @@ func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) e return f.PrintingKubeClient.Wait(resources, d) } +// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. +func (f *FailingKubeClient) WaitWithRetry(resources kube.ResourceList, d time.Duration, waitRetries int) error { + time.Sleep(f.WaitDuration) + if f.WaitError != nil { + return f.WaitError + } + return f.PrintingKubeClient.WaitWithRetry(resources, d, waitRetries) +} + // WaitWithJobs returns the configured error if set or prints func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { if f.WaitError != nil { @@ -82,6 +91,14 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur return f.PrintingKubeClient.WaitWithJobs(resources, d) } +// WaitWithJobs returns the configured error if set or prints +func (f *FailingKubeClient) WaitWithJobsWithRetry(resources kube.ResourceList, d time.Duration, waitRetries int) error { + if f.WaitError != nil { + return f.WaitError + } + return f.PrintingKubeClient.WaitWithJobsWithRetry(resources, d, waitRetries) +} + // WaitForDelete returns the configured error if set or prints func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error { if f.WaitError != nil { diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index e6c4b6207..23c746686 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -62,11 +62,21 @@ func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) return err } +func (p *PrintingKubeClient) WaitWithRetry(resources kube.ResourceList, _ time.Duration, _ int) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } +func (p *PrintingKubeClient) WaitWithJobsWithRetry(resources kube.ResourceList, _ time.Duration, _ int) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index ce42ed950..8c799985f 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -110,7 +110,19 @@ type InterfaceResources interface { BuildTable(reader io.Reader, validate bool) (ResourceList, error) } +// InterfaceWithRetry is introduced to avoid breaking backwards compatibility for Interface implementers. +// +// TODO Helm 4: Remove InterfaceWithRetry and integrate its method(s) into the Interface. +type InterfaceWithRetry interface { + // Wait waits up to the given timeout for the specified resources to be ready. + WaitWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error + + // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. + WaitWithJobsWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error +} + var _ Interface = (*Client)(nil) var _ InterfaceExt = (*Client)(nil) var _ InterfaceDeletionPropagation = (*Client)(nil) var _ InterfaceResources = (*Client)(nil) +var _ InterfaceWithRetry = (*Client)(nil) diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index ecdd38940..a392c99c9 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -32,6 +32,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/resource" "k8s.io/apimachinery/pkg/util/wait" ) @@ -42,18 +43,35 @@ type waiter struct { log func(string, ...interface{}) } -// waitForResources polls to get the current status of all pods, PVCs, Services and -// Jobs(optional) until all are ready or a timeout is reached -func (w *waiter) waitForResources(created ResourceList) error { - w.log("beginning wait for %d resources with timeout of %v", len(created), w.timeout) +// Jobs(optional) until all are ready or a timeout is reached. +// If an error is encountered when checking on the status of a resource then retries +// will be performed subject to the given maximum number of retries +func (w *waiter) waitForResources(created ResourceList, waitRetries int) error { + w.log("beginning wait for %d resources with timeout of %v, maximum retries %d", len(created), w.timeout, waitRetries) ctx, cancel := context.WithTimeout(context.Background(), w.timeout) defer cancel() + numberOfErrors := make([]int, len(created)) + for i := range numberOfErrors { + numberOfErrors[i] = 0 + } + return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { - for _, v := range created { + for i, v := range created { ready, err := w.c.IsReady(ctx, v) - if !ready || err != nil { + + if waitRetries > 0 && w.isRetryableError(err, v) { + numberOfErrors[i]++ + if numberOfErrors[i] > waitRetries { + w.log("Max number of retries reached") + return false, err + } + w.log("Retrying as current number of retries %d less than max number of retries %d", numberOfErrors[i]-1, waitRetries) + return false, nil + } + numberOfErrors[i] = 0 + if !ready { return false, err } } @@ -61,6 +79,21 @@ func (w *waiter) waitForResources(created ResourceList) error { }) } +func (w *waiter) isRetryableError(err error, resource *resource.Info) bool { + if err == nil { + return false + } + w.log("Error received when checking status of resource %s. Error: '%s', Resource details: '%s'", resource.Name, err, resource) + if ev, ok := err.(*apierrors.StatusError); ok { + statusCode := ev.Status().Code + retryable := statusCode >= 500 + w.log("Status code received: %d. Retryable error? %t", statusCode, retryable) + return retryable + } + w.log("Retryable error? %t", true) + return true +} + // waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached func (w *waiter) waitForDeletedResources(deleted ResourceList) error { w.log("beginning wait for %d resources to be deleted with timeout of %v", len(deleted), w.timeout) From d2cf8c66f1775783edbc150d1a509f58e769e75e Mon Sep 17 00:00:00 2001 From: MichaelMorris Date: Thu, 13 Jul 2023 17:46:03 +0100 Subject: [PATCH 02/18] Updated for review comments Signed-off-by: MichaelMorris --- cmd/helm/install.go | 1 - cmd/helm/upgrade.go | 2 -- pkg/action/install.go | 20 +++++------------ pkg/action/install_test.go | 16 ------------- pkg/action/upgrade.go | 24 +++++++------------- pkg/action/upgrade_test.go | 46 -------------------------------------- pkg/kube/client.go | 18 ++------------- pkg/kube/fake/fake.go | 17 -------------- pkg/kube/fake/printer.go | 10 --------- pkg/kube/interface.go | 12 ---------- pkg/kube/wait.go | 17 +++++++++----- 11 files changed, 27 insertions(+), 156 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ae68be121..6ffc968ce 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -167,7 +167,6 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state 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.IntVar(&client.WaitRetries, "wait-retries", 0, "if set and --wait enabled, will retry any failed check on resource state subject to the specified number of retries") f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.StringVar(&client.Description, "description", "", "add a custom description") diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 8b7634619..145d342b7 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -117,7 +117,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Timeout = client.Timeout instClient.Wait = client.Wait instClient.WaitForJobs = client.WaitForJobs - instClient.WaitRetries = client.WaitRetries instClient.Devel = client.Devel instClient.Namespace = client.Namespace instClient.Atomic = client.Atomic @@ -234,7 +233,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state 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.IntVar(&client.WaitRetries, "wait-retries", 0, "if set and --wait enabled, will retry any failed check on resource state, except if HTTP status code < 500 is received, subject to the specified number of retries") f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically if --atomic is used") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") diff --git a/pkg/action/install.go b/pkg/action/install.go index 63abc7dbd..5fb7e6736 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -77,7 +77,6 @@ type Install struct { Replace bool Wait bool WaitForJobs bool - WaitRetries int Devel bool DependencyUpdate bool Timeout time.Duration @@ -414,24 +413,17 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t } if i.Wait { - var err error if i.WaitForJobs { - if kubeClient, ok := i.cfg.KubeClient.(kube.InterfaceWithRetry); ok { - err = kubeClient.WaitWithJobsWithRetry(resources, i.Timeout, i.WaitRetries) - } else { - err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) + if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { + i.reportToRun(c, rel, err) + return } } else { - if kubeClient, ok := i.cfg.KubeClient.(kube.InterfaceWithRetry); ok { - err = kubeClient.WaitWithRetry(resources, i.Timeout, i.WaitRetries) - } else { - err = i.cfg.KubeClient.Wait(resources, i.Timeout) + if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { + i.reportToRun(c, rel, err) + return } } - if err != nil { - i.reportToRun(c, rel, err) - return - } } if !i.DisableHooks { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 438f12439..dd0cdb54d 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -393,22 +393,6 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled") is.Equal(res.Info.Status, release.StatusFailed) } -func TestInstallRelease_Wait_With_Retries(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "come-fail-away" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - instAction.cfg.KubeClient = failer - instAction.Wait = true - instAction.WaitRetries = 2 - vals := map[string]interface{}{} - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index a35fe90ff..784df4013 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -68,8 +68,6 @@ type Upgrade struct { Wait bool // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. WaitForJobs bool - // WaitRetries determines whether any failed resource state checks will be retried during the wait operation. - WaitRetries int // DisableHooks disables hook processing if set to true. DisableHooks bool // DryRun controls whether the operation is prepared, but not executed. @@ -404,25 +402,19 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele u.cfg.Log( "waiting for release %s resources (created: %d updated: %d deleted: %d)", upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted)) - var err error if u.WaitForJobs { - if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceWithRetry); ok { - err = kubeClient.WaitWithJobsWithRetry(target, u.Timeout, u.WaitRetries) - } else { - err = u.cfg.KubeClient.WaitWithJobs(target, u.Timeout) + if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } } else { - if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceWithRetry); ok { - err = kubeClient.WaitWithRetry(target, u.Timeout, u.WaitRetries) - } else { - err = u.cfg.KubeClient.Wait(target, u.Timeout) + if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } } - if err != nil { - u.cfg.recordRelease(originalRelease) - u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) - return - } } // post-upgrade hooks diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 62e4fa411..62922b373 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -88,28 +88,6 @@ func TestUpgradeRelease_Wait(t *testing.T) { is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) } -func TestUpgradeRelease_Wait_With_Retries(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - upAction.cfg.KubeClient = failer - upAction.Wait = true - upAction.WaitRetries = 2 - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} func TestUpgradeRelease_WaitForJobs(t *testing.T) { is := assert.New(t) @@ -134,30 +112,6 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { is.Equal(res.Info.Status, release.StatusFailed) } -func TestUpgradeRelease_WaitForJobs_With_Retries(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - upAction.cfg.KubeClient = failer - upAction.Wait = true - upAction.WaitForJobs = true - upAction.WaitRetries = 2 - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} - func TestUpgradeRelease_CleanupOnFail(t *testing.T) { is := assert.New(t) req := require.New(t) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index a2c1a7a40..7b3c803f9 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -275,13 +275,6 @@ func getResource(info *resource.Info) (runtime.Object, error) { // Wait waits up to the given timeout for the specified resources to be ready. func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { - return c.WaitWithRetry(resources, timeout, 0) -} - -// WaitWithRetry waits up to the given timeout for the specified resources to be ready. If an error -// is encountered when checking on the status of a resource then retries will be performed subject -// to the given maximum number of retries -func (c *Client) WaitWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error { cs, err := c.getKubeClient() if err != nil { return err @@ -292,18 +285,11 @@ func (c *Client) WaitWithRetry(resources ResourceList, timeout time.Duration, wa log: c.Log, timeout: timeout, } - return w.waitForResources(resources, waitRetries) + return w.waitForResources(resources) } // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) error { - return c.WaitWithJobsWithRetry(resources, timeout, 0) -} - -// WaitWithJobsWithRetry waits up to the given timeout for the specified resources to be ready, including jobs. -// If an error is encountered when checking on the status of a resource then retries will be performed subject -// to the given maximum number of retries -func (c *Client) WaitWithJobsWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error { cs, err := c.getKubeClient() if err != nil { return err @@ -314,7 +300,7 @@ func (c *Client) WaitWithJobsWithRetry(resources ResourceList, timeout time.Dura log: c.Log, timeout: timeout, } - return w.waitForResources(resources, waitRetries) + return w.waitForResources(resources) } // WaitForDelete wait up to the given timeout for the specified resources to be deleted. diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index 4b351fa70..267020d57 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -74,15 +74,6 @@ func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) e return f.PrintingKubeClient.Wait(resources, d) } -// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. -func (f *FailingKubeClient) WaitWithRetry(resources kube.ResourceList, d time.Duration, waitRetries int) error { - time.Sleep(f.WaitDuration) - if f.WaitError != nil { - return f.WaitError - } - return f.PrintingKubeClient.WaitWithRetry(resources, d, waitRetries) -} - // WaitWithJobs returns the configured error if set or prints func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { if f.WaitError != nil { @@ -91,14 +82,6 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur return f.PrintingKubeClient.WaitWithJobs(resources, d) } -// WaitWithJobs returns the configured error if set or prints -func (f *FailingKubeClient) WaitWithJobsWithRetry(resources kube.ResourceList, d time.Duration, waitRetries int) error { - if f.WaitError != nil { - return f.WaitError - } - return f.PrintingKubeClient.WaitWithJobsWithRetry(resources, d, waitRetries) -} - // WaitForDelete returns the configured error if set or prints func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error { if f.WaitError != nil { diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index 23c746686..e6c4b6207 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -62,21 +62,11 @@ func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) return err } -func (p *PrintingKubeClient) WaitWithRetry(resources kube.ResourceList, _ time.Duration, _ int) error { - _, err := io.Copy(p.Out, bufferize(resources)) - return err -} - func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } -func (p *PrintingKubeClient) WaitWithJobsWithRetry(resources kube.ResourceList, _ time.Duration, _ int) error { - _, err := io.Copy(p.Out, bufferize(resources)) - return err -} - func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 8c799985f..ce42ed950 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -110,19 +110,7 @@ type InterfaceResources interface { BuildTable(reader io.Reader, validate bool) (ResourceList, error) } -// InterfaceWithRetry is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceWithRetry and integrate its method(s) into the Interface. -type InterfaceWithRetry interface { - // Wait waits up to the given timeout for the specified resources to be ready. - WaitWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error - - // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. - WaitWithJobsWithRetry(resources ResourceList, timeout time.Duration, waitRetries int) error -} - var _ Interface = (*Client)(nil) var _ InterfaceExt = (*Client)(nil) var _ InterfaceDeletionPropagation = (*Client)(nil) var _ InterfaceResources = (*Client)(nil) -var _ InterfaceWithRetry = (*Client)(nil) diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index a392c99c9..6b747d551 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -19,6 +19,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube" import ( "context" "fmt" + "net/http" "time" "github.com/pkg/errors" @@ -43,11 +44,10 @@ type waiter struct { log func(string, ...interface{}) } -// Jobs(optional) until all are ready or a timeout is reached. -// If an error is encountered when checking on the status of a resource then retries -// will be performed subject to the given maximum number of retries -func (w *waiter) waitForResources(created ResourceList, waitRetries int) error { - w.log("beginning wait for %d resources with timeout of %v, maximum retries %d", len(created), w.timeout, waitRetries) +// waitForResources polls to get the current status of all pods, PVCs, Services and +// Jobs(optional) until all are ready or a timeout is reached +func (w *waiter) waitForResources(created ResourceList) error { + w.log("beginning wait for %d resources with timeout of %v", len(created), w.timeout) ctx, cancel := context.WithTimeout(context.Background(), w.timeout) defer cancel() @@ -58,6 +58,7 @@ func (w *waiter) waitForResources(created ResourceList, waitRetries int) error { } return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + waitRetries := 30 for i, v := range created { ready, err := w.c.IsReady(ctx, v) @@ -86,7 +87,7 @@ func (w *waiter) isRetryableError(err error, resource *resource.Info) bool { w.log("Error received when checking status of resource %s. Error: '%s', Resource details: '%s'", resource.Name, err, resource) if ev, ok := err.(*apierrors.StatusError); ok { statusCode := ev.Status().Code - retryable := statusCode >= 500 + retryable := w.isRetryableHTTPStatusCode(statusCode) w.log("Status code received: %d. Retryable error? %t", statusCode, retryable) return retryable } @@ -94,6 +95,10 @@ func (w *waiter) isRetryableError(err error, resource *resource.Info) bool { return true } +func (w *waiter) isRetryableHTTPStatusCode(httpStatusCode int32) bool { + return httpStatusCode == 0 || httpStatusCode == http.StatusTooManyRequests || (httpStatusCode >= 500 && httpStatusCode != http.StatusNotImplemented) +} + // waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached func (w *waiter) waitForDeletedResources(deleted ResourceList) error { w.log("beginning wait for %d resources to be deleted with timeout of %v", len(deleted), w.timeout) From cbab6d6227969435df516dcdfcc6d29808aff094 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:36:23 +0000 Subject: [PATCH 03/18] chore(deps): bump actions/setup-go from 4.1.0 to 5.0.0 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.1.0 to 5.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4.1.0...0c52d547c9bc32b1aa3301fd7a9cb496313a4491) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index dcc982dc9..1a8b1dd03 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1 - name: Setup Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # pin@5.0.0 with: go-version: "1.21" - name: golangci-lint From 94c1deae6d5a43491c5a4e8444ecd8273a8122a1 Mon Sep 17 00:00:00 2001 From: Ricardo Maraschini Date: Wed, 31 Jan 2024 12:48:22 +0100 Subject: [PATCH 04/18] bug: add proxy support for oci getter adds missing proxy support on oci chart getter. Signed-off-by: Ricardo Maraschini --- pkg/getter/ocigetter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index 209786bd7..0547cdcbb 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -119,6 +119,7 @@ func (g *OCIGetter) newRegistryClient() (*registry.Client, error) { IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, + Proxy: http.ProxyFromEnvironment, } }) From 8e6a5149d2e2e3beffa51d53048b2fed90d8c529 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 7 Feb 2024 10:54:15 -0500 Subject: [PATCH 05/18] validation fix Signed-off-by: Matt Farina --- pkg/chart/metadata.go | 6 ++++ pkg/chart/metadata_test.go | 5 ++++ pkg/chartutil/errors.go | 8 +++++ pkg/chartutil/save.go | 20 +++++++++++++ pkg/chartutil/save_test.go | 29 +++++++++++++++++++ pkg/downloader/manager_test.go | 26 +++++++++++++++++ pkg/lint/rules/chartfile.go | 4 +++ pkg/lint/rules/chartfile_test.go | 8 +++++ .../rules/testdata/badchartname/Chart.yaml | 5 ++++ .../rules/testdata/badchartname/values.yaml | 1 + 10 files changed, 112 insertions(+) create mode 100644 pkg/lint/rules/testdata/badchartname/Chart.yaml create mode 100644 pkg/lint/rules/testdata/badchartname/values.yaml diff --git a/pkg/chart/metadata.go b/pkg/chart/metadata.go index 97bfc2c0c..a08a97cd1 100644 --- a/pkg/chart/metadata.go +++ b/pkg/chart/metadata.go @@ -16,6 +16,7 @@ limitations under the License. package chart import ( + "path/filepath" "strings" "unicode" @@ -110,6 +111,11 @@ func (md *Metadata) Validate() error { if md.Name == "" { return ValidationError("chart.metadata.name is required") } + + if md.Name != filepath.Base(md.Name) { + return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) + } + if md.Version == "" { return ValidationError("chart.metadata.version is required") } diff --git a/pkg/chart/metadata_test.go b/pkg/chart/metadata_test.go index c8b89ae55..62aea7261 100644 --- a/pkg/chart/metadata_test.go +++ b/pkg/chart/metadata_test.go @@ -40,6 +40,11 @@ func TestValidate(t *testing.T) { &Metadata{APIVersion: "v2", Version: "1.0"}, ValidationError("chart.metadata.name is required"), }, + { + "chart without name", + &Metadata{Name: "../../test", APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name \"../../test\" is invalid"), + }, { "chart without version", &Metadata{Name: "test", APIVersion: "v2"}, diff --git a/pkg/chartutil/errors.go b/pkg/chartutil/errors.go index fcdcc27ea..0a4046d2e 100644 --- a/pkg/chartutil/errors.go +++ b/pkg/chartutil/errors.go @@ -33,3 +33,11 @@ type ErrNoValue struct { } func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } + +type ErrInvalidChartName struct { + Name string +} + +func (e ErrInvalidChartName) Error() string { + return fmt.Sprintf("%q is not a valid chart name", e.Name) +} diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index 2ce4eddaf..4ee90709c 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -39,6 +39,10 @@ var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") // directory, writing the chart's contents to that subdirectory. func SaveDir(c *chart.Chart, dest string) error { // Create the chart directory + err := validateName(c.Name()) + if err != nil { + return err + } outdir := filepath.Join(dest, c.Name()) if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() { return errors.Errorf("file %s already exists and is not a directory", outdir) @@ -149,6 +153,10 @@ func Save(c *chart.Chart, outDir string) (string, error) { } func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { + err := validateName(c.Name()) + if err != nil { + return err + } base := filepath.Join(prefix, c.Name()) // Pull out the dependencies of a v1 Chart, since there's no way @@ -242,3 +250,15 @@ func writeToTar(out *tar.Writer, name string, body []byte) error { _, err := out.Write(body) return err } + +// If the name has directory name has characters which would change the location +// they need to be removed. +func validateName(name string) error { + nname := filepath.Base(name) + + if nname != name { + return ErrInvalidChartName{name} + } + + return nil +} diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index db485c7cb..98b4e641b 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -106,6 +106,24 @@ func TestSave(t *testing.T) { } }) } + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "../ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*chart.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + }, + } + _, err := Save(c, tmp) + if err == nil { + t.Fatal("Expected error saving chart with invalid name") + } } // Creates a copy with a different schema; does not modify anything. @@ -232,4 +250,15 @@ func TestSaveDir(t *testing.T) { if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name { t.Fatal("Files data did not match") } + + tmp2 := t.TempDir() + c.Metadata.Name = "../ahab" + pth := filepath.Join(tmp2, "tmpcharts") + if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil { + t.Fatal(err) + } + + if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" { + t.Fatalf("Did not get expected error for chart named %q", c.Name()) + } } diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index f7ab1a568..db2487d16 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -262,6 +262,32 @@ func TestDownloadAll(t *testing.T) { if _, err := os.Stat(filepath.Join(chartPath, "charts", "signtest-0.1.0.tgz")); os.IsNotExist(err) { t.Error(err) } + + // A chart with a bad name like this cannot be loaded and saved. Handling in + // the loading and saving will return an error about the invalid name. In + // this case, the chart needs to be created directly. + badchartyaml := `apiVersion: v2 +description: A Helm chart for Kubernetes +name: ../bad-local-subchart +version: 0.1.0` + if err := os.MkdirAll(filepath.Join(chartPath, "testdata", "bad-local-subchart"), 0755); err != nil { + t.Fatal(err) + } + err = os.WriteFile(filepath.Join(chartPath, "testdata", "bad-local-subchart", "Chart.yaml"), []byte(badchartyaml), 0644) + if err != nil { + t.Fatal(err) + } + + badLocalDep := &chart.Dependency{ + Name: "../bad-local-subchart", + Repository: "file://./testdata/bad-local-subchart", + Version: "0.1.0", + } + + err = m.downloadAll([]*chart.Dependency{badLocalDep}) + if err == nil { + t.Fatal("Expected error for bad dependency name") + } } func TestUpdateBeforeBuild(t *testing.T) { diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 70532ad4f..910602b7d 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -106,6 +106,10 @@ func validateChartName(cf *chart.Metadata) error { if cf.Name == "" { return errors.New("name is required") } + name := filepath.Base(cf.Name) + if name != cf.Name { + return fmt.Errorf("chart name %q is invalid", cf.Name) + } return nil } diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index 087cda047..a06d7dc31 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -30,16 +30,19 @@ import ( ) const ( + badCharNametDir = "testdata/badchartname" badChartDir = "testdata/badchartfile" anotherBadChartDir = "testdata/anotherbadchartfile" ) var ( + badChartNamePath = filepath.Join(badCharNametDir, "Chart.yaml") badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") ) var badChart, _ = chartutil.LoadChartfile(badChartFilePath) +var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) // Validation functions Test func TestValidateChartYamlNotDirectory(t *testing.T) { @@ -69,6 +72,11 @@ func TestValidateChartName(t *testing.T) { if err == nil { t.Errorf("validateChartName to return a linter error, got no error") } + + err = validateChartName(badChartName) + if err == nil { + t.Error("expected validateChartName to return a linter error for an invalid name, got no error") + } } func TestValidateChartVersion(t *testing.T) { diff --git a/pkg/lint/rules/testdata/badchartname/Chart.yaml b/pkg/lint/rules/testdata/badchartname/Chart.yaml new file mode 100644 index 000000000..64f8fb8bf --- /dev/null +++ b/pkg/lint/rules/testdata/badchartname/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: "../badchartname" +type: application diff --git a/pkg/lint/rules/testdata/badchartname/values.yaml b/pkg/lint/rules/testdata/badchartname/values.yaml new file mode 100644 index 000000000..9f367033b --- /dev/null +++ b/pkg/lint/rules/testdata/badchartname/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. From 68294fdae0deba2464805067228790e025207ebd Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Thu, 8 Feb 2024 09:08:09 -0800 Subject: [PATCH 06/18] Fix: Ignore alias validation error for index load Signed-off-by: George Jenkins --- pkg/repo/index.go | 22 ++++++++++++++++++- pkg/repo/index_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 8a23ba060..2f062b028 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -362,7 +362,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { if cvs[idx].APIVersion == "" { cvs[idx].APIVersion = chart.APIVersionV1 } - if err := cvs[idx].Validate(); err != nil { + if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil { log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err) cvs = append(cvs[:idx], cvs[idx+1:]...) } @@ -388,3 +388,23 @@ func jsonOrYamlUnmarshal(b []byte, i interface{}) error { } return yaml.UnmarshalStrict(b, i) } + +// ignoreSkippableChartValidationError inspect the given error and returns nil if +// the error isn't important for index loading +// +// In particular, charts may introduce validations that don't impact repository indexes +// And repository indexes may be generated by older/non-complient software, which doesn't +// conform to all validations. +func ignoreSkippableChartValidationError(err error) error { + verr, ok := err.(chart.ValidationError) + if !ok { + return err + } + + // https://github.com/helm/helm/issues/12748 (JFrog repository strips alias field) + if strings.HasPrefix(verr.Error(), "validation: more than one dependency with name or alias") { + return nil + } + + return err +} diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index cb9317f7e..9652b1f21 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -20,6 +20,7 @@ import ( "bufio" "bytes" "encoding/json" + "fmt" "net/http" "os" "path/filepath" @@ -592,3 +593,50 @@ func TestAddFileIndexEntriesNil(t *testing.T) { } } } + +func TestIgnoreSkippableChartValidationError(t *testing.T) { + type TestCase struct { + Input error + ErrorSkipped bool + } + testCases := map[string]TestCase{ + "nil": { + Input: nil, + }, + "generic_error": { + Input: fmt.Errorf("foo"), + }, + "non_skipped_validation_error": { + Input: chart.ValidationError("chart.metadata.type must be application or library"), + }, + "skipped_validation_error": { + Input: chart.ValidationErrorf("more than one dependency with name or alias %q", "foo"), + ErrorSkipped: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := ignoreSkippableChartValidationError(tc.Input) + + if tc.Input == nil { + if result != nil { + t.Error("") + } + return + } + + if tc.ErrorSkipped { + if result != nil { + t.Error("") + } + return + } + + if tc.Input != result { + t.Error("") + } + + }) + } +} From 8d19bcb78aaeb489eba4ed1d68894e59c8f55876 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Thu, 8 Feb 2024 11:07:36 -0800 Subject: [PATCH 07/18] add error messages Signed-off-by: George Jenkins --- pkg/repo/index_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 9652b1f21..3852dc15d 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -621,20 +621,20 @@ func TestIgnoreSkippableChartValidationError(t *testing.T) { if tc.Input == nil { if result != nil { - t.Error("") + t.Error("expected nil result for nil input") } return } if tc.ErrorSkipped { if result != nil { - t.Error("") + t.Error("expected nil result for skipped error") } return } if tc.Input != result { - t.Error("") + t.Error("expected the result equal to input") } }) From e6db0ec933582a0de2f55f8f2f1ef693739eedc0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 21:17:34 +0000 Subject: [PATCH 08/18] chore(deps): bump golangci/golangci-lint-action from 3.7.0 to 4.0.0 Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.7.0 to 4.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/3a919529898de77ec3da873e3063ca4b10e7f5cc...3cfe3a4abbb849e10058ce4af15d205b6da42804) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/golangci-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index dcc982dc9..ea082872e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -17,6 +17,6 @@ jobs: with: go-version: "1.21" - name: golangci-lint - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc #pin@3.7.0 + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 #pin@4.0.0 with: version: v1.55 From 5bc97b9c4eff3d2968d3c74c64b25052140558d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:53:16 +0000 Subject: [PATCH 09/18] chore(deps): bump github/codeql-action from 3.23.1 to 3.24.3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.23.1 to 3.24.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/0b21cf2492b6b02c465a3e5d7c473717ad7721ba...379614612a29c9e28f31f39a59013eb8012a51f0) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1942471bf..ded77cbee 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # pinv3.23.1 + uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # pinv3.23.1 + uses: github/codeql-action/autobuild@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # pinv3.23.1 + uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 From 764557c470533fa57aad99f865c9ff75a64d4163 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 21 Feb 2024 09:45:58 -0500 Subject: [PATCH 10/18] Some fixes Signed-off-by: Matt Farina --- pkg/plugin/plugin.go | 4 ++++ pkg/plugin/plugin_test.go | 6 ++++++ pkg/repo/index.go | 4 ++++ pkg/repo/index_test.go | 4 ++++ 4 files changed, 18 insertions(+) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index fb3bc5215..5bb743481 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -175,6 +175,10 @@ var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$") // validatePluginData validates a plugin's YAML data. func validatePluginData(plug *Plugin, filepath string) error { + // When metadata section missing, initialize with no data + if plug.Metadata == nil { + plug.Metadata = &Metadata{} + } if !validPluginName.MatchString(plug.Metadata.Name) { return fmt.Errorf("invalid plugin name at %q", filepath) } diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 1ec2d5304..725052346 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -353,6 +353,11 @@ func TestSetupEnvWithSpace(t *testing.T) { } func TestValidatePluginData(t *testing.T) { + // A mock plugin missing any metadata. + mockMissingMeta := &Plugin{ + Dir: "no-such-dir", + } + for i, item := range []struct { pass bool plug *Plugin @@ -363,6 +368,7 @@ func TestValidatePluginData(t *testing.T) { {false, mockPlugin("$foo -bar")}, // Test leading chars {false, mockPlugin("foo -bar ")}, // Test trailing chars {false, mockPlugin("foo\nbar")}, // Test newline + {false, mockMissingMeta}, // Test if the metadata section missing } { err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i)) if item.pass && err != nil { diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 2f062b028..40b11c5cf 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -359,6 +359,10 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source) continue } + // When metadata section missing, initialize with no data + if cvs[idx].Metadata == nil { + cvs[idx].Metadata = &chart.Metadata{} + } if cvs[idx].APIVersion == "" { cvs[idx].APIVersion = chart.APIVersionV1 } diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 3852dc15d..91486670b 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -70,6 +70,10 @@ entries: name: grafana foo: - + bar: + - digest: "sha256:1234567890abcdef" + urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz ` ) From 4f200fa74f4b1bc8ad7261afb30ae7e2a8f0f546 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:37:12 +0000 Subject: [PATCH 11/18] chore(deps): bump github/codeql-action from 3.24.3 to 3.24.5 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.3 to 3.24.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/379614612a29c9e28f31f39a59013eb8012a51f0...47b3d888fe66b639e431abf22ebca059152f1eea) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ded77cbee..1be802ccc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 + uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 + uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@379614612a29c9e28f31f39a59013eb8012a51f0 # pinv3.24.3 + uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 From e22d881495fcfee6b1c4afa1d12627f3e28b54e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:34:11 +0000 Subject: [PATCH 12/18] chore(deps): bump github/codeql-action from 3.24.5 to 3.24.6 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.5 to 3.24.6. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/47b3d888fe66b639e431abf22ebca059152f1eea...8a470fddafa5cbb6266ee11b37ef4d8aae19c571) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1be802ccc..c6dc2b998 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 + uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 + uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # pinv3.24.5 + uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 From 8b424baea1e40a352acf549395e6498e63ac0aa2 Mon Sep 17 00:00:00 2001 From: Robert Sirchia Date: Fri, 1 Mar 2024 15:16:24 -0500 Subject: [PATCH 13/18] Updating .gitignore Adding a line to ignore the .devcontainer/ to the .gitignore file so I can use containers to develop with Helm. Signed-off-by: Robert Sirchia --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ef9806616..cdb68ceb5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .idea/ .vimrc .vscode/ +.devcontainer/ _dist/ _dist_versions/ bin/ From 275f2ab43b86072a601d036acc6d7eb2bb501b08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 22:01:21 +0000 Subject: [PATCH 14/18] chore(deps): bump golang.org/x/term from 0.15.0 to 0.18.0 Bumps [golang.org/x/term](https://github.com/golang/term) from 0.15.0 to 0.18.0. - [Commits](https://github.com/golang/term/compare/v0.15.0...v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e200d4fcb..2270faabb 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/crypto v0.17.0 - golang.org/x/term v0.15.0 + golang.org/x/term v0.18.0 golang.org/x/text v0.14.0 k8s.io/api v0.29.0 k8s.io/apiextensions-apiserver v0.29.0 @@ -150,7 +150,7 @@ require ( golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect diff --git a/go.sum b/go.sum index 2799262df..b3941a045 100644 --- a/go.sum +++ b/go.sum @@ -483,14 +483,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From a23dd9e3b756c12cfdaa1b2c3a023c92530c0d0a Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Sun, 10 Mar 2024 14:05:50 -0400 Subject: [PATCH 15/18] Add a note about --dry-run displaying secrets Signed-off-by: Matt Farina --- cmd/helm/install.go | 6 +++++- cmd/helm/upgrade.go | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index d987d300f..84ecc406d 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -94,7 +94,11 @@ And in the following example, 'foo' is set to '{"key1":"value1","key2":"bar"}': $ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis To check the generated manifests of a release without installing the chart, -the '--debug' and '--dry-run' flags can be combined. +the --debug and --dry-run flags can be combined. + +The --dry-run flag will output all generated chart manifests, including Secrets +which can contain sensitive values. Please carefully consider how and when this +flag is used. If --verify is set, the chart MUST have a provenance file, and the provenance file MUST pass all verification steps. diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index e7c6dd166..acd1c72f1 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -72,6 +72,10 @@ parameters, and existing values will be merged with any values set via '--values or '--set' flags. Priority is given to new values. $ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis + +The --dry-run flag will output all generated chart manifests, including Secrets +which can contain sensitive values. Please carefully consider how and when this +flag is used. ` func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { From ff94e9319104a58321444d7d4656917147058936 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:48:58 +0000 Subject: [PATCH 16/18] chore(deps): bump github/codeql-action from 3.24.6 to 3.24.7 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.24.6 to 3.24.7. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8a470fddafa5cbb6266ee11b37ef4d8aae19c571...3ab4101902695724f9365a384f86c1074d94e18c) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c6dc2b998..c467cf724 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 + uses: github/codeql-action/init@3ab4101902695724f9365a384f86c1074d94e18c # pinv3.24.7 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 + uses: github/codeql-action/autobuild@3ab4101902695724f9365a384f86c1074d94e18c # pinv3.24.7 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # pinv3.24.6 + uses: github/codeql-action/analyze@3ab4101902695724f9365a384f86c1074d94e18c # pinv3.24.7 From 25c473834e0cbb905fc8a524709fd4d5362dab11 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Tue, 12 Mar 2024 13:56:10 -0400 Subject: [PATCH 17/18] Enabling hide secrets on install and upgrade dry run This change adds a new flag to the install and upgrade commands in the Helm client and properties to the install and upgrade action. The new flag is --hide-secret and can only be used with the --dry-run flag. The --dry-run flag is designed to send all chart rendered manifests to stdout so that they can be inspected. When the --hide-secret flag is used the Secret content is removed from the output. Signed-off-by: Matt Farina --- cmd/helm/install.go | 8 +- cmd/helm/install_test.go | 16 +++ .../install-dry-run-with-secret-hidden.txt | 20 ++++ .../output/install-dry-run-with-secret.txt | 25 +++++ .../testdata/output/install-hide-secret.txt | 1 + .../testcharts/chart-with-secret/Chart.yaml | 4 + .../templates/configmap.yaml | 6 ++ .../chart-with-secret/templates/secret.yaml | 6 ++ cmd/helm/upgrade.go | 6 +- cmd/helm/upgrade_test.go | 101 ++++++++++++++++++ pkg/action/action.go | 8 +- pkg/action/action_test.go | 7 ++ pkg/action/install.go | 20 ++-- pkg/action/install_test.go | 40 +++++++ pkg/action/upgrade.go | 10 +- pkg/action/upgrade_test.go | 51 +++++++++ 16 files changed, 316 insertions(+), 13 deletions(-) create mode 100644 cmd/helm/testdata/output/install-dry-run-with-secret-hidden.txt create mode 100644 cmd/helm/testdata/output/install-dry-run-with-secret.txt create mode 100644 cmd/helm/testdata/output/install-hide-secret.txt create mode 100644 cmd/helm/testdata/testcharts/chart-with-secret/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/chart-with-secret/templates/configmap.yaml create mode 100644 cmd/helm/testdata/testcharts/chart-with-secret/templates/secret.yaml diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 84ecc406d..9a304395c 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -97,8 +97,8 @@ To check the generated manifests of a release without installing the chart, the --debug and --dry-run flags can be combined. The --dry-run flag will output all generated chart manifests, including Secrets -which can contain sensitive values. Please carefully consider how and when this -flag is used. +which can contain sensitive values. To hide Kubernetes Secrets use the +--hide-secret flag. Please carefully consider how and when these flags are used. If --verify is set, the chart MUST have a provenance file, and the provenance file MUST pass all verification steps. @@ -163,6 +163,10 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } addInstallFlags(cmd, cmd.Flags(), client, valueOpts) + // hide-secret is not available in all places the install flags are used so + // it is added separately + f := cmd.Flags() + f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer) diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index b34d1455c..6da3df0f9 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -252,6 +252,22 @@ func TestInstall(t *testing.T) { cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()), golden: "output/install.txt", }, + { + name: "dry-run displaying secret", + cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run", + golden: "output/install-dry-run-with-secret.txt", + }, + { + name: "dry-run hiding secret", + cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run --hide-secret", + golden: "output/install-dry-run-with-secret-hidden.txt", + }, + { + name: "hide-secret error without dry-run", + cmd: "install secrets testdata/testcharts/chart-with-secret --hide-secret", + wantError: true, + golden: "output/install-hide-secret.txt", + }, } runTestCmd(t, tests) diff --git a/cmd/helm/testdata/output/install-dry-run-with-secret-hidden.txt b/cmd/helm/testdata/output/install-dry-run-with-secret-hidden.txt new file mode 100644 index 000000000..2f928c3dc --- /dev/null +++ b/cmd/helm/testdata/output/install-dry-run-with-secret-hidden.txt @@ -0,0 +1,20 @@ +NAME: secrets +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: pending-install +REVISION: 1 +TEST SUITE: None +HOOKS: +MANIFEST: +--- +# Source: chart-with-secret/templates/secret.yaml +# HIDDEN: The Secret output has been suppressed +--- +# Source: chart-with-secret/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap +data: + foo: bar + diff --git a/cmd/helm/testdata/output/install-dry-run-with-secret.txt b/cmd/helm/testdata/output/install-dry-run-with-secret.txt new file mode 100644 index 000000000..6c094ef6a --- /dev/null +++ b/cmd/helm/testdata/output/install-dry-run-with-secret.txt @@ -0,0 +1,25 @@ +NAME: secrets +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: pending-install +REVISION: 1 +TEST SUITE: None +HOOKS: +MANIFEST: +--- +# Source: chart-with-secret/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: test-secret +stringData: + foo: bar +--- +# Source: chart-with-secret/templates/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap +data: + foo: bar + diff --git a/cmd/helm/testdata/output/install-hide-secret.txt b/cmd/helm/testdata/output/install-hide-secret.txt new file mode 100644 index 000000000..aaf73b478 --- /dev/null +++ b/cmd/helm/testdata/output/install-hide-secret.txt @@ -0,0 +1 @@ +Error: INSTALLATION FAILED: Hiding Kubernetes secrets requires a dry-run mode diff --git a/cmd/helm/testdata/testcharts/chart-with-secret/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-secret/Chart.yaml new file mode 100644 index 000000000..46d069e1c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-secret/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Chart with Kubernetes Secret +name: chart-with-secret +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-secret/templates/configmap.yaml b/cmd/helm/testdata/testcharts/chart-with-secret/templates/configmap.yaml new file mode 100644 index 000000000..ce9c27d56 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-secret/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap +data: + foo: bar diff --git a/cmd/helm/testdata/testcharts/chart-with-secret/templates/secret.yaml b/cmd/helm/testdata/testcharts/chart-with-secret/templates/secret.yaml new file mode 100644 index 000000000..b1e1cff56 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-secret/templates/secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-secret +stringData: + foo: bar diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index acd1c72f1..79463acce 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -74,8 +74,8 @@ or '--set' flags. Priority is given to new values. $ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis The --dry-run flag will output all generated chart manifests, including Secrets -which can contain sensitive values. Please carefully consider how and when this -flag is used. +which can contain sensitive values. To hide Kubernetes Secrets use the +--hide-secret flag. Please carefully consider how and when these flags are used. ` func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -146,6 +146,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.DependencyUpdate = client.DependencyUpdate instClient.Labels = client.Labels instClient.EnableDNS = client.EnableDNS + instClient.HideSecret = client.HideSecret rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { @@ -246,6 +247,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") + f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") f.Lookup("dry-run").NoOptDefVal = "client" f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 485267d1d..64cd6e417 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -458,3 +458,104 @@ func TestUpgradeInstallWithLabels(t *testing.T) { t.Errorf("Expected {%v}, got {%v}", expectedLabels, updatedRel.Labels) } } + +func prepareMockReleaseWithSecret(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { + tmpChart := t.TempDir() + configmapData, err := os.ReadFile("testdata/testcharts/chart-with-secret/templates/configmap.yaml") + if err != nil { + t.Fatalf("Error loading template yaml %v", err) + } + secretData, err := os.ReadFile("testdata/testcharts/chart-with-secret/templates/secret.yaml") + if err != nil { + t.Fatalf("Error loading template yaml %v", err) + } + cfile := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + }, + Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, + } + chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart for upgrade: %v", err) + } + ch, err := loader.Load(chartPath) + if err != nil { + t.Fatalf("Error loading chart: %v", err) + } + _ = release.Mock(&release.MockReleaseOptions{ + Name: releaseName, + Chart: ch, + }) + + relMock := func(n string, v int, ch *chart.Chart) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) + } + + return relMock, ch, chartPath +} + +func TestUpgradeWithDryRun(t *testing.T) { + releaseName := "funny-bunny-labels" + _, _, chartPath := prepareMockReleaseWithSecret(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + // First install a release into the store so that future --dry-run attempts + // have it available. + cmd := fmt.Sprintf("upgrade %s --install '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + _, err = store.Get(releaseName, 1) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + cmd = fmt.Sprintf("upgrade %s --dry-run '%s'", releaseName, chartPath) + _, out, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + // No second release should be stored because this is a dry run. + _, err = store.Get(releaseName, 2) + if err == nil { + t.Error("expected error as there should be no new release but got none") + } + + if !strings.Contains(out, "kind: Secret") { + t.Error("expected secret in output from --dry-run but found none") + } + + // Ensure the secret is not in the output + cmd = fmt.Sprintf("upgrade %s --dry-run --hide-secret '%s'", releaseName, chartPath) + _, out, err = executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + // No second release should be stored because this is a dry run. + _, err = store.Get(releaseName, 2) + if err == nil { + t.Error("expected error as there should be no new release but got none") + } + + if strings.Contains(out, "kind: Secret") { + t.Error("expected no secret in output from --dry-run --hide-secret but found one") + } + + // Ensure there is an error when --hide-secret used without dry-run + cmd = fmt.Sprintf("upgrade %s --hide-secret '%s'", releaseName, chartPath) + _, _, err = executeActionCommandC(store, cmd) + if err == nil { + t.Error("expected error when --hide-secret used without --dry-run") + } +} diff --git a/pkg/action/action.go b/pkg/action/action.go index 5693f4838..863c48f07 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -103,7 +103,7 @@ type Configuration struct { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) { +func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { hs := []*release.Hook{} b := bytes.NewBuffer(nil) @@ -200,7 +200,11 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu for _, m := range manifests { if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" { + fmt.Fprintf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name) + } else { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + } } else { newDir := outputDir if useReleaseName { diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index c4ef6c056..fdcfa7558 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -195,6 +195,13 @@ func withSampleTemplates() chartOption { } } +func withSampleSecret() chartOption { + return func(opts *chartOptions) { + sampleSecret := &chart.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} + opts.Templates = append(opts.Templates, sampleSecret) + } +} + func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { sampleTemplates := []*chart.File{ diff --git a/pkg/action/install.go b/pkg/action/install.go index e3538a4f5..de612e3b7 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -69,11 +69,14 @@ type Install struct { ChartPathOptions - ClientOnly bool - Force bool - CreateNamespace bool - DryRun bool - DryRunOption string + ClientOnly bool + Force bool + CreateNamespace bool + DryRun bool + DryRunOption string + // HideSecret can be set to true when DryRun is enabled in order to hide + // Kubernetes Secrets in the output. It cannot be used outside of DryRun. + HideSecret bool DisableHooks bool Replace bool Wait bool @@ -230,6 +233,11 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma } } + // HideSecret must be used with dry run. Otherwise, return an error. + if !i.isDryRun() && i.HideSecret { + return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode") + } + if err := i.availableName(); err != nil { return nil, err } @@ -301,7 +309,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 118e4b366..69b9cbc48 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -255,6 +255,46 @@ func TestInstallRelease_DryRun(t *testing.T) { is.Equal(res.Info.Description, "Dry run complete") } +func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + + // First perform a normal dry-run with the secret and confirm its presence. + instAction.DryRun = true + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + is.Contains(res.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret") + + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Equal(res.Info.Description, "Dry run complete") + + // Perform a dry-run where the secret should not be present + instAction.HideSecret = true + vals = map[string]interface{}{} + res2, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.NotContains(res2.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret") + + _, err = instAction.cfg.Releases.Get(res2.Name, res2.Version) + is.Error(err) + is.Equal(res2.Info.Description, "Dry run complete") + + // Ensure there is an error when HideSecret True but not in a dry-run mode + instAction.DryRun = false + vals = map[string]interface{}{} + _, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) + if err == nil { + t.Fatalf("Did not get expected an error when dry-run false and hide secret is true") + } +} + // Regression test for #7955 func TestInstallRelease_DryRun_Lookup(t *testing.T) { is := assert.New(t) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index ffb7538a6..2bd40a850 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -74,6 +74,9 @@ type Upgrade struct { DryRun bool // DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster. DryRunOption string + // HideSecret can be set to true when DryRun is enabled in order to hide + // Kubernetes Secrets in the output. It cannot be used outside of DryRun. + HideSecret bool // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // // This should be used with caution. @@ -191,6 +194,11 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, errMissingChart } + // HideSecret must be used with dry run. Otherwise, return an error. + if !u.isDryRun() && u.HideSecret { + return nil, nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode") + } + // finds the last non-deleted release with the given name lastRelease, err := u.cfg.Releases.Last(name) if err != nil { @@ -259,7 +267,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin interactWithRemote = true } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret) if err != nil { return nil, nil, err } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index e259605ce..78b4347e3 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -535,3 +535,54 @@ func TestUpgradeRelease_SystemLabels(t *testing.T) { is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err) } + +func TestUpgradeRelease_DryRun(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "previous-release" + rel.Info.Status = release.StatusDeployed + req.NoError(upAction.cfg.Releases.Create(rel)) + + upAction.DryRun = true + vals := map[string]interface{}{} + + ctx, done := context.WithCancel(context.Background()) + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) + done() + req.NoError(err) + is.Equal(release.StatusPendingUpgrade, res.Info.Status) + is.Contains(res.Manifest, "kind: Secret") + + lastRelease, err := upAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + is.Equal(lastRelease.Info.Status, release.StatusDeployed) + is.Equal(1, lastRelease.Version) + + // Test the case for hiding the secret to ensure it is not displayed + upAction.HideSecret = true + vals = map[string]interface{}{} + + ctx, done = context.WithCancel(context.Background()) + res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) + done() + req.NoError(err) + is.Equal(release.StatusPendingUpgrade, res.Info.Status) + is.NotContains(res.Manifest, "kind: Secret") + + lastRelease, err = upAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + is.Equal(lastRelease.Info.Status, release.StatusDeployed) + is.Equal(1, lastRelease.Version) + + // Ensure in a dry run mode when using HideSecret + upAction.DryRun = false + vals = map[string]interface{}{} + + ctx, done = context.WithCancel(context.Background()) + _, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) + done() + req.Error(err) +} From 0a69a0dea6b1dcebaaf5d5b67c9a56eade463a71 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Fri, 22 Mar 2024 06:20:01 -0500 Subject: [PATCH 18/18] Modified how created annotation is populated based on package creation time Signed-off-by: Andrew Block --- pkg/pusher/ocipusher.go | 4 ++++ pkg/registry/client.go | 14 ++++++------ pkg/registry/util.go | 12 +++++----- pkg/registry/util_test.go | 41 ++++++++++++++++++++++++++++++++--- pkg/registry/utils_test.go | 27 +++++++++++++---------- pkg/time/ctime/ctime.go | 25 +++++++++++++++++++++ pkg/time/ctime/ctime_linux.go | 30 +++++++++++++++++++++++++ pkg/time/ctime/ctime_other.go | 27 +++++++++++++++++++++++ 8 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 pkg/time/ctime/ctime.go create mode 100644 pkg/time/ctime/ctime_linux.go create mode 100644 pkg/time/ctime/ctime_other.go diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go index 94154d389..b37a0c605 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/time/ctime" ) // OCIPusher is the default OCI backend handler @@ -89,6 +90,9 @@ func (pusher *OCIPusher) push(chartRef, href string) error { path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name), meta.Metadata.Version) + chartCreationTime := ctime.Created(stat) + pushOpts = append(pushOpts, registry.PushOptCreationTime(chartCreationTime.Format(time.RFC3339))) + _, err = client.Push(chartBytes, ref, pushOpts...) return err } diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 7538cf69b..a287afaf7 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -527,9 +527,9 @@ type ( } pushOperation struct { - provData []byte - strictMode bool - test bool + provData []byte + strictMode bool + creationTime string } ) @@ -583,7 +583,7 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu descriptors = append(descriptors, provDescriptor) } - ociAnnotations := generateOCIAnnotations(meta, operation.test) + ociAnnotations := generateOCIAnnotations(meta, operation.creationTime) manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...) if err != nil { @@ -652,10 +652,10 @@ func PushOptStrictMode(strictMode bool) PushOption { } } -// PushOptTest returns a function that sets whether test setting on push -func PushOptTest(test bool) PushOption { +// PushOptCreationDate returns a function that sets the creation time +func PushOptCreationTime(creationTime string) PushOption { return func(operation *pushOperation) { - operation.test = test + operation.creationTime = creationTime } } diff --git a/pkg/registry/util.go b/pkg/registry/util.go index 8baf0852a..4bff39495 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -166,10 +166,10 @@ func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, i } // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest -func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { +func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { // Get annotations from Chart attributes - ociAnnotations := generateChartOCIAnnotations(meta, test) + ociAnnotations := generateChartOCIAnnotations(meta, creationTime) // Copy Chart annotations annotations: @@ -190,7 +190,7 @@ annotations: } // getChartOCIAnnotations will generate OCI annotations from the provided chart -func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { +func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { chartOCIAnnotations := map[string]string{} chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description) @@ -198,10 +198,12 @@ func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]str chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version) chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) - if !test { - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339)) + if len(creationTime) == 0 { + creationTime = helmtime.Now().UTC().Format(time.RFC3339) } + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) + if len(meta.Sources) > 0 { chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0]) } diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go index fdf09360b..908ea4950 100644 --- a/pkg/registry/util_test.go +++ b/pkg/registry/util_test.go @@ -29,6 +29,8 @@ import ( func TestGenerateOCIChartAnnotations(t *testing.T) { + nowString := helmtime.Now().Format(time.RFC3339) + tests := []struct { name string chart *chart.Metadata @@ -43,6 +45,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, }, }, { @@ -56,6 +59,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, "org.opencontainers.image.description": "OCI Helm Chart", "org.opencontainers.image.url": "https://helm.sh", }, @@ -76,6 +80,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, "org.opencontainers.image.description": "OCI Helm Chart", "org.opencontainers.image.url": "https://helm.sh", "org.opencontainers.image.authors": "John Snow", @@ -95,6 +100,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, "org.opencontainers.image.description": "OCI Helm Chart", "org.opencontainers.image.url": "https://helm.sh", "org.opencontainers.image.authors": "John Snow (john@winterfell.com)", @@ -115,6 +121,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, "org.opencontainers.image.description": "OCI Helm Chart", "org.opencontainers.image.url": "https://helm.sh", "org.opencontainers.image.authors": "John Snow (john@winterfell.com), Jane Snow", @@ -133,6 +140,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, "org.opencontainers.image.description": "OCI Helm Chart", "org.opencontainers.image.source": "https://github.com/helm/helm", }, @@ -141,7 +149,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { for _, tt := range tests { - result := generateChartOCIAnnotations(tt.chart, true) + result := generateChartOCIAnnotations(tt.chart, nowString) if !reflect.DeepEqual(tt.expect, result) { t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) @@ -152,6 +160,8 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { func TestGenerateOCIAnnotations(t *testing.T) { + nowString := helmtime.Now().Format(time.RFC3339) + tests := []struct { name string chart *chart.Metadata @@ -166,6 +176,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { map[string]string{ "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, }, }, { @@ -183,6 +194,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.created": nowString, "extrakey": "extravlue", "anotherkey": "anothervalue", }, @@ -203,6 +215,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { "org.opencontainers.image.title": "oci", "org.opencontainers.image.version": "0.0.1", "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.created": nowString, "extrakey": "extravlue", }, }, @@ -210,7 +223,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { for _, tt := range tests { - result := generateOCIAnnotations(tt.chart, true) + result := generateOCIAnnotations(tt.chart, nowString) if !reflect.DeepEqual(tt.expect, result) { t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) @@ -220,12 +233,16 @@ func TestGenerateOCIAnnotations(t *testing.T) { } func TestGenerateOCICreatedAnnotations(t *testing.T) { + + nowTime := helmtime.Now() + nowTimeString := nowTime.Format(time.RFC3339) + chart := &chart.Metadata{ Name: "oci", Version: "0.0.1", } - result := generateOCIAnnotations(chart, false) + result := generateOCIAnnotations(chart, nowTimeString) // Check that created annotation exists if _, ok := result[ocispec.AnnotationCreated]; !ok { @@ -237,4 +254,22 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) } + // Verify default creation time set + result = generateOCIAnnotations(chart, "") + + // Check that created annotation exists + if _, ok := result[ocispec.AnnotationCreated]; !ok { + t.Errorf("%s annotation not created", ocispec.AnnotationCreated) + } + + if createdTimeAnnotation, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) + + // Verify creation annotation after time test began + if !nowTime.Before(createdTimeAnnotation) { + t.Errorf("%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString) + } + + } + } diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go index 74aa0dbc0..d7aba2bb7 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/utils_test.go @@ -216,9 +216,12 @@ func initCompromisedRegistryTestServer() string { } func testPush(suite *TestSuite) { + + testingChartCreationTime := "1977-09-02T22:04:05Z" + // Bad bytes ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) - _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true)) + _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime)) suite.NotNil(err, "error pushing non-chart bytes") // Load a test chart @@ -229,20 +232,20 @@ func testPush(suite *TestSuite) { // non-strict ref (chart name) ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) suite.NotNil(err, "error pushing non-strict ref (bad basename)") // non-strict ref (chart name), with strict mode disabled - _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") // non-strict ref (chart version) ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) suite.NotNil(err, "error pushing non-strict ref (bad tag)") // non-strict ref (chart version), with strict mode disabled - _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") // basic push, good ref @@ -251,7 +254,7 @@ func testPush(suite *TestSuite) { meta, err = extractChartMeta(chartData) suite.Nil(err, "no error extracting chart meta") ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) suite.Nil(err, "no error pushing good ref") _, err = suite.RegistryClient.Pull(ref) @@ -269,7 +272,7 @@ func testPush(suite *TestSuite) { // push with prov ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptTest(true)) + result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime)) suite.Nil(err, "no error pushing good ref with prov") _, err = suite.RegistryClient.Pull(ref) @@ -281,12 +284,12 @@ func testPush(suite *TestSuite) { suite.Equal(ref, result.Ref) suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(684), result.Manifest.Size) + suite.Equal(int64(742), result.Manifest.Size) suite.Equal(int64(99), result.Config.Size) suite.Equal(int64(973), result.Chart.Size) suite.Equal(int64(695), result.Prov.Size) suite.Equal( - "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", + "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2", result.Manifest.Digest) suite.Equal( "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", @@ -354,12 +357,12 @@ func testPull(suite *TestSuite) { suite.Equal(ref, result.Ref) suite.Equal(meta.Name, result.Chart.Meta.Name) suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(684), result.Manifest.Size) + suite.Equal(int64(742), result.Manifest.Size) suite.Equal(int64(99), result.Config.Size) suite.Equal(int64(973), result.Chart.Size) suite.Equal(int64(695), result.Prov.Size) suite.Equal( - "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", + "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2", result.Manifest.Digest) suite.Equal( "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", @@ -370,7 +373,7 @@ func testPull(suite *TestSuite) { suite.Equal( "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", result.Prov.Digest) - suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}", + suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}", string(result.Manifest.Data)) suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}", string(result.Config.Data)) diff --git a/pkg/time/ctime/ctime.go b/pkg/time/ctime/ctime.go new file mode 100644 index 000000000..f2998d76d --- /dev/null +++ b/pkg/time/ctime/ctime.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package ctime + +import ( + "os" + "time" +) + +func Created(fi os.FileInfo) time.Time { + return created(fi) +} diff --git a/pkg/time/ctime/ctime_linux.go b/pkg/time/ctime/ctime_linux.go new file mode 100644 index 000000000..c3cea1d78 --- /dev/null +++ b/pkg/time/ctime/ctime_linux.go @@ -0,0 +1,30 @@ +//go:build linux + +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package ctime + +import ( + "os" + "syscall" + "time" +) + +func created(fi os.FileInfo) time.Time { + st := fi.Sys().(*syscall.Stat_t) + //nolint + return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec)) +} diff --git a/pkg/time/ctime/ctime_other.go b/pkg/time/ctime/ctime_other.go new file mode 100644 index 000000000..f21ed7347 --- /dev/null +++ b/pkg/time/ctime/ctime_other.go @@ -0,0 +1,27 @@ +//go:build !linux + +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package ctime + +import ( + "os" + "time" +) + +func created(fi os.FileInfo) time.Time { + return fi.ModTime() +}