fix: give helm a scratch directory instead of $HOME

Helm resolves its cache, config and data homes from `$HOME`, but dokku runs `scheduler-k3s:initialize` as root and every other subcommand as the dokku user, so those paths are not always readable by the user a command ends up as. Helm treats a repositories file it cannot read as a fatal error rather than an absent one, which made chart resolution fail outright even though dokku never reads an entry from that file. Each call now points helm at a private scratch directory that is removed once the chart has been located.
This commit is contained in:
Jose Diaz-Gonzalez
2026-09-18 17:45:43 -04:00
parent cf5487c2df
commit 47f1ca90a9
3 changed files with 152 additions and 4 deletions

View File

@@ -17,6 +17,7 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
@@ -101,6 +102,60 @@ func applyChartPathOptions(options *action.ChartPathOptions, input ChartInput) {
}
}
// helmHomeEnvVars are the environment variables helm resolves its cache, config
// and data homes from, mapped to the scratch subdirectory each one gets.
var helmHomeEnvVars = map[string]string{
helmpath.CacheHomeEnvVar: "cache",
helmpath.ConfigHomeEnvVar: "config",
helmpath.DataHomeEnvVar: "data",
}
// newHelmSettings returns helm settings backed by a private scratch directory,
// along with a function that removes it and restores the environment.
//
// helm otherwise resolves its cache, config and data homes from $HOME. Dokku
// runs scheduler-k3s:initialize as root and every other subcommand as the dokku
// user, so $HOME is not always readable by the user a command ends up as, and
// helm treats a repositories file it cannot read as a fatal error rather than
// an absent one. Nothing dokku keeps there needs to outlive a command: every
// chart carries its own repository url, and helm re-fetches both the repository
// index and the chart archive on every call.
func newHelmSettings() (*cli.EnvSettings, func(), error) {
directory, err := os.MkdirTemp("", "dokku-helm-")
if err != nil {
return nil, nil, fmt.Errorf("Error creating helm scratch directory: %w", err)
}
restore := map[string]*string{}
cleanup := func() {
for envVar, previous := range restore {
if previous == nil {
os.Unsetenv(envVar) // nolint: errcheck
continue
}
os.Setenv(envVar, *previous) // nolint: errcheck
}
os.RemoveAll(directory) // nolint: errcheck
}
for envVar, subdirectory := range helmHomeEnvVars {
if previous, ok := os.LookupEnv(envVar); ok {
restore[envVar] = &previous
} else {
restore[envVar] = nil
}
if err := os.Setenv(envVar, filepath.Join(directory, subdirectory)); err != nil {
cleanup()
return nil, nil, fmt.Errorf("Error setting %s: %w", envVar, err)
}
}
return cli.New(), cleanup, nil
}
type HelmAgent struct {
Configuration *action.Configuration
Namespace string
@@ -231,7 +286,12 @@ func (h *HelmAgent) InstallChart(ctx context.Context, input ChartInput) error {
applyChartPathOptions(&client.ChartPathOptions, input)
settings := cli.New()
settings, cleanup, err := newHelmSettings()
if err != nil {
return err
}
defer cleanup()
chart, err := client.ChartPathOptions.LocateChart(input.ChartPath, settings)
if err != nil {
return fmt.Errorf("Error locating chart: %w", err)
@@ -357,7 +417,12 @@ func (h *HelmAgent) UpgradeChart(ctx context.Context, input ChartInput) error {
applyChartPathOptions(&client.ChartPathOptions, input)
settings := cli.New()
settings, cleanup, err := newHelmSettings()
if err != nil {
return err
}
defer cleanup()
chart, err := client.ChartPathOptions.LocateChart(input.ChartPath, settings)
if err != nil {
return fmt.Errorf("Error locating chart: %w", err)

View File

@@ -1,9 +1,13 @@
package scheduler_k3s
import (
"os"
"path/filepath"
"strings"
"testing"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/helmpath"
)
func TestApplyChartPathOptions(t *testing.T) {
@@ -145,3 +149,78 @@ func TestSelectRevisions(t *testing.T) {
})
}
}
func TestNewHelmSettings(t *testing.T) {
sentinel := map[string]string{
helmpath.CacheHomeEnvVar: "/sentinel/cache",
helmpath.ConfigHomeEnvVar: "/sentinel/config",
helmpath.DataHomeEnvVar: "/sentinel/data",
}
for envVar, value := range sentinel {
t.Setenv(envVar, value)
}
settings, cleanup, err := newHelmSettings()
if err != nil {
t.Fatalf("newHelmSettings() returned an error: %v", err)
}
directory := os.Getenv(helmpath.CacheHomeEnvVar)
if directory == sentinel[helmpath.CacheHomeEnvVar] {
t.Fatalf("%s was not overridden", helmpath.CacheHomeEnvVar)
}
scratch := filepath.Dir(directory)
if _, err := os.Stat(scratch); err != nil {
t.Fatalf("scratch directory %q is not usable: %v", scratch, err)
}
for envVar, subdirectory := range helmHomeEnvVars {
want := filepath.Join(scratch, subdirectory)
if got := os.Getenv(envVar); got != want {
t.Errorf("%s = %q, want %q", envVar, got, want)
}
}
paths := map[string]string{
"RepositoryConfig": settings.RepositoryConfig,
"RepositoryCache": settings.RepositoryCache,
"RegistryConfig": settings.RegistryConfig,
}
for name, path := range paths {
if !strings.HasPrefix(path, scratch+string(os.PathSeparator)) {
t.Errorf("%s = %q, want a path under %q", name, path, scratch)
}
}
cleanup()
if _, err := os.Stat(scratch); !os.IsNotExist(err) {
t.Errorf("scratch directory %q still exists after cleanup", scratch)
}
for envVar, want := range sentinel {
if got := os.Getenv(envVar); got != want {
t.Errorf("%s = %q after cleanup, want the prior value %q", envVar, got, want)
}
}
}
func TestNewHelmSettingsUnsetsVariablesThatWereNotSet(t *testing.T) {
for envVar := range helmHomeEnvVars {
t.Setenv(envVar, "")
os.Unsetenv(envVar) // nolint: errcheck
}
_, cleanup, err := newHelmSettings()
if err != nil {
t.Fatalf("newHelmSettings() returned an error: %v", err)
}
cleanup()
for envVar := range helmHomeEnvVars {
if value, ok := os.LookupEnv(envVar); ok {
t.Errorf("%s = %q after cleanup, want it to be unset", envVar, value)
}
}
}

View File

@@ -15,7 +15,6 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
@@ -108,7 +107,12 @@ func CommandPreview(appName string, diffContext int, showSecrets bool, showSecre
}
func loadChartForPreview(chartPath string) (*chart.Chart, error) {
settings := cli.New()
settings, cleanup, err := newHelmSettings()
if err != nil {
return nil, err
}
defer cleanup()
opts := action.ChartPathOptions{}
located, err := opts.LocateChart(chartPath, settings)
if err != nil {