address helm chart verification, auth, and tls options (#601)

Signed-off-by: Adam Martin <adam.martin@ranchergovernment.com>
This commit is contained in:
Adam Martin
2026-08-02 23:41:23 -04:00
committed by GitHub
parent e3764e8378
commit 2e280f2717
7 changed files with 179 additions and 6 deletions
+4 -4
View File
@@ -140,18 +140,18 @@ jobs:
# verify via helm repository
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.4
hauler store add chart rancher --repo https://releases.rancher.com/server-charts/stable --version 2.8.3 --verify
# verify via oci helm repository
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --version 1.0.6
hauler store add chart hauler-helm --repo oci://ghcr.io/hauler-dev --version 1.0.4 --verify
# verify via local helm repository
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.2/rancher-cluster-templates-0.5.2.tgz
hauler store add chart rancher-cluster-templates-0.5.2.tgz --repo .
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.1/rancher-cluster-templates-0.5.1.tgz
hauler store add chart rancher-cluster-templates-0.5.1.tgz --repo . --version 0.5.1
curl -sfOL https://github.com/rancherfederal/rancher-cluster-templates/releases/download/rancher-cluster-templates-0.5.0/rancher-cluster-templates-0.5.0.tgz
hauler store add chart rancher-cluster-templates-0.5.0.tgz --repo . --version 0.5.0 --verify
curl -sfLO https://raw.githubusercontent.com/helm/helm/v3.18.6/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz
curl -sfLO https://raw.githubusercontent.com/helm/helm/v3.18.6/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov
curl -sfLO https://raw.githubusercontent.com/helm/helm/v3.18.6/cmd/helm/testdata/helm-test-key.pub
hauler store add chart signtest-0.1.0.tgz --repo . --version 0.1.0 --verify --keyring helm-test-key.pub
# verify via the hauler store contents
hauler store info
+36 -2
View File
@@ -509,11 +509,26 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
platform = ch.Platform
}
chartUsername, chartPassword, err := resolveChartCreds(ch)
if err != nil {
return err
}
if err := storeChart(ctx, s, ch,
&flags.AddChartOpts{
ChartOpts: &action.ChartPathOptions{
RepoURL: ch.RepoURL,
Version: ch.Version,
RepoURL: ch.RepoURL,
Version: ch.Version,
Verify: ch.Verify,
Keyring: ch.Keyring,
Username: chartUsername,
Password: chartPassword,
PassCredentialsAll: ch.PassCredentialsAll,
CertFile: ch.CertFile,
KeyFile: ch.KeyFile,
CaFile: ch.CaFile,
InsecureSkipTLSVerify: ch.InsecureSkipTLSVerify,
PlainHTTP: ch.PlainHTTP,
},
AddImages: ch.AddImages,
AddDependencies: ch.AddDependencies,
@@ -540,6 +555,25 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
return nil
}
// resolveChartCreds reads credentials for a Chart entry from the env vars
// named by UsernameEnv and PasswordEnv. Both fields must be set or both must
// be empty; a mix is a configuration error. If both are set, the env vars
// must be non-empty at runtime.
func resolveChartCreds(ch v1.Chart) (username, password string, err error) {
if ch.UsernameEnv == "" && ch.PasswordEnv == "" {
return "", "", nil
}
if ch.UsernameEnv == "" || ch.PasswordEnv == "" {
return "", "", fmt.Errorf("chart %q: usernameEnv and passwordEnv must both be set or both be empty", ch.Name)
}
username = os.Getenv(ch.UsernameEnv)
password = os.Getenv(ch.PasswordEnv)
if username == "" || password == "" {
return "", "", fmt.Errorf("chart %q: env vars %q and %q must both be set and non-empty", ch.Name, ch.UsernameEnv, ch.PasswordEnv)
}
return username, password, nil
}
func processImageTxt(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
l := log.FromContext(ctx)
l.Infof("syncing images from [%s] to store", filepath.Base(fi.Name()))
+90
View File
@@ -20,6 +20,7 @@ import (
"github.com/rs/zerolog"
"hauler.dev/go/hauler/v2/internal/flags"
v1 "hauler.dev/go/hauler/v2/pkg/apis/hauler.cattle.io/v1"
"hauler.dev/go/hauler/v2/pkg/consts"
)
@@ -49,6 +50,95 @@ func newSyncOpts(storeDir string) *flags.SyncOpts {
}
}
// --------------------------------------------------------------------------
// resolveChartCreds tests
// --------------------------------------------------------------------------
func TestResolveChartCreds_BothEmpty(t *testing.T) {
ch := v1.Chart{Name: "mychart", RepoURL: "https://charts.example.com"}
u, p, err := resolveChartCreds(ch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if u != "" || p != "" {
t.Errorf("expected empty creds, got username=%q password=%q", u, p)
}
}
func TestResolveChartCreds_BothSetAndEnvPopulated(t *testing.T) {
t.Setenv("CHART_TEST_USER", "alice")
t.Setenv("CHART_TEST_PASS", "s3cr3t")
ch := v1.Chart{
Name: "mychart",
RepoURL: "https://charts.example.com",
UsernameEnv: "CHART_TEST_USER",
PasswordEnv: "CHART_TEST_PASS",
}
u, p, err := resolveChartCreds(ch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if u != "alice" {
t.Errorf("username: got %q, want %q", u, "alice")
}
if p != "s3cr3t" {
t.Errorf("password: got %q, want %q", p, "s3cr3t")
}
}
func TestResolveChartCreds_OnlyUsernameEnvSet_ReturnsError(t *testing.T) {
ch := v1.Chart{
Name: "mychart",
RepoURL: "https://charts.example.com",
UsernameEnv: "CHART_TEST_USER_ONLY",
// PasswordEnv intentionally omitted
}
_, _, err := resolveChartCreds(ch)
if err == nil {
t.Fatal("expected error when only usernameEnv is set, got nil")
}
if !strings.Contains(err.Error(), "usernameEnv and passwordEnv must both be set") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestResolveChartCreds_OnlyPasswordEnvSet_ReturnsError(t *testing.T) {
ch := v1.Chart{
Name: "mychart",
RepoURL: "https://charts.example.com",
// UsernameEnv intentionally omitted
PasswordEnv: "CHART_TEST_PASS_ONLY",
}
_, _, err := resolveChartCreds(ch)
if err == nil {
t.Fatal("expected error when only passwordEnv is set, got nil")
}
if !strings.Contains(err.Error(), "usernameEnv and passwordEnv must both be set") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestResolveChartCreds_EnvVarUnset_ReturnsError(t *testing.T) {
// Ensure the env vars are definitely absent.
t.Setenv("CHART_UNSET_USER", "")
t.Setenv("CHART_UNSET_PASS", "")
ch := v1.Chart{
Name: "mychart",
RepoURL: "https://charts.example.com",
UsernameEnv: "CHART_UNSET_USER",
PasswordEnv: "CHART_UNSET_PASS",
}
_, _, err := resolveChartCreds(ch)
if err == nil {
t.Fatal("expected error when env vars are empty, got nil")
}
if !strings.Contains(err.Error(), "must both be set and non-empty") {
t.Errorf("unexpected error message: %v", err)
}
}
// --------------------------------------------------------------------------
// processContent tests
// --------------------------------------------------------------------------
+1
View File
@@ -66,6 +66,7 @@ func (o *AddChartOpts) AddFlags(cmd *cobra.Command) {
f.StringVar(&o.ChartOpts.RepoURL, "repo", "", "Location of the chart (https:// | http:// | oci://)")
f.StringVar(&o.ChartOpts.Version, "version", "", "(Optional) Specify the version of the chart (v1.0.0 | 2.0.0 | ^2.0.0)")
f.BoolVar(&o.ChartOpts.Verify, "verify", false, "(Optional) Verify the chart before fetching it")
f.StringVar(&o.ChartOpts.Keyring, "keyring", "", "(Optional) Location of public keyring used by --verify (default: $HOME/.gnupg/pubring.gpg)")
f.StringVar(&o.ChartOpts.Username, "username", "", "(Optional) Username to use for authentication")
f.StringVar(&o.ChartOpts.Password, "password", "", "(Optional) Password to use for authentication")
f.StringVar(&o.ChartOpts.CertFile, "cert-file", "", "(Optional) Location of the TLS Certificate to use for client authentication")
+17
View File
@@ -26,4 +26,21 @@ type Chart struct {
AddImages bool `json:"add-images,omitempty"`
AddDependencies bool `json:"add-dependencies,omitempty"`
ExcludeExtras bool `json:"exclude-extras,omitempty"`
// Verification
Verify bool `json:"verify,omitempty"`
Keyring string `json:"keyring,omitempty"`
// Auth (HTTP repos only — for OCI registries use `hauler login`)
// Credentials are referenced by env-var name; raw values must NOT appear in manifests.
UsernameEnv string `json:"usernameEnv,omitempty"`
PasswordEnv string `json:"passwordEnv,omitempty"`
PassCredentialsAll bool `json:"passCredentialsAll,omitempty"`
// TLS
CertFile string `json:"certFile,omitempty"`
KeyFile string `json:"keyFile,omitempty"`
CaFile string `json:"caFile,omitempty"`
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
PlainHTTP bool `json:"plainHTTP,omitempty"`
}
+14
View File
@@ -50,7 +50,21 @@ func NewChart(name string, opts *action.ChartPathOptions) (*Chart, error) {
}
client := action.NewInstall(actionConfig)
// Propagate auth, TLS, and verification options from the caller.
// RepoURL is intentionally NOT copied here — it is set conditionally below
// based on URL scheme (OCI vs HTTP vs bare).
client.ChartPathOptions.Version = opts.Version
client.ChartPathOptions.Verify = opts.Verify
client.ChartPathOptions.Keyring = opts.Keyring
client.ChartPathOptions.Username = opts.Username
client.ChartPathOptions.Password = opts.Password
client.ChartPathOptions.PassCredentialsAll = opts.PassCredentialsAll
client.ChartPathOptions.CertFile = opts.CertFile
client.ChartPathOptions.KeyFile = opts.KeyFile
client.ChartPathOptions.CaFile = opts.CaFile
client.ChartPathOptions.InsecureSkipTLSVerify = opts.InsecureSkipTLSVerify
client.ChartPathOptions.PlainHTTP = opts.PlainHTTP
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP)
+17
View File
@@ -3,6 +3,7 @@ package chart_test
import (
"os"
"reflect"
"strings"
"testing"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -129,3 +130,19 @@ func TestNewChart(t *testing.T) {
})
}
}
func TestNewChart_VerifyOnUnsignedChartFails(t *testing.T) {
_, err := chart.NewChart(
"rancher-cluster-templates-0.5.2.tgz",
&action.ChartPathOptions{
RepoURL: "../../../testdata",
Verify: true,
},
)
if err == nil {
t.Fatalf("expected verify failure on unsigned chart, got nil error")
}
if !strings.Contains(err.Error(), "provenance") {
t.Fatalf("expected provenance error, got: %v", err)
}
}