mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 04:16:27 +00:00
address helm chart verification, auth, and tls options (#601)
Signed-off-by: Adam Martin <adam.martin@ranchergovernment.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user