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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user