Merge branch 'main' into added-hauler-copy-cmd

This commit is contained in:
Zack Brady
2026-08-13 14:30:21 -04:00
committed by GitHub
12 changed files with 527 additions and 206 deletions
+15 -17
View File
@@ -3,7 +3,6 @@ package cli
import (
"fmt"
"os"
"strconv"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
@@ -78,11 +77,18 @@ func addStoreSync(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
if o.CaFile == "" {
o.CaFile = os.Getenv(consts.CaFile)
}
if o.InsecureSkipTLSVerify == nil {
if v := os.Getenv(consts.InsecureSkipTLSVerify); v != "" {
b, _ := strconv.ParseBool(v)
o.InsecureSkipTLSVerify = &b
}
// record which precedence-carrying flags the user explicitly set, so
// the resolvers can let an explicit CLI value win over per-item/annotation
o.TlogChanged = cmd.Flags().Changed("use-tlog-verify")
o.ExcludeExtrasChanged = cmd.Flags().Changed("exclude-extras")
o.InsecureChanged = cmd.Flags().Changed("insecure-skip-tls-verify")
o.StoreChanged = cmd.Flags().Changed("store")
o.RetriesChanged = cmd.Flags().Changed("retries")
// env var only applies when the flag wasn't set, so an explicit --insecure-skip-tls-verify=false still wins
if !o.InsecureChanged && os.Getenv(consts.InsecureSkipTLSVerify) == "true" {
o.InsecureSkipTLSVerify = true
}
// --dry-run requires --products
@@ -124,12 +130,6 @@ func addStoreSync(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
}
rso.BlobConcurrency = bc
// resolve *bool: nil unless the user explicitly passed the flag
if cmd.Flags().Changed("insecure-skip-tls-verify") {
v, _ := cmd.Flags().GetBool("insecure-skip-tls-verify")
o.InsecureSkipTLSVerify = &v
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
@@ -411,11 +411,9 @@ func addStoreAddImage(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Co
if o.CaFile == "" {
o.CaFile = os.Getenv(consts.CaFile)
}
if o.InsecureSkipTLSVerify == nil {
if v := os.Getenv(consts.InsecureSkipTLSVerify); v != "" {
b, _ := strconv.ParseBool(v)
o.InsecureSkipTLSVerify = &b
}
// env var only applies when the flag wasn't set, so an explicit --insecure-skip-tls-verify=false still wins
if !cmd.Flags().Changed("insecure-skip-tls-verify") && os.Getenv(consts.InsecureSkipTLSVerify) == "true" {
o.InsecureSkipTLSVerify = true
}
return nil
},
+36 -31
View File
@@ -59,7 +59,7 @@ func AddFileCmd(ctx context.Context, o *flags.AddFileOpts, s *store.Layout, refe
cfg := v1.File{
Path: reference,
CaFile: o.CaFile,
InsecureSkipTLSVerify: &o.InsecureSkipTLSVerify,
InsecureSkipTLSVerify: o.InsecureSkipTLSVerify,
}
if len(o.Name) > 0 {
cfg.Name = o.Name
@@ -83,7 +83,7 @@ func storeFile(ctx context.Context, s *store.Layout, fi v1.File, ro *flags.CliRo
copts := getter.ClientOptions{
NameOverride: fi.Name,
InsecureSkipTLSVerify: derefInsecure(fi.InsecureSkipTLSVerify),
InsecureSkipTLSVerify: fi.InsecureSkipTLSVerify,
CAFile: fi.CaFile,
}
@@ -248,7 +248,7 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re
func addImageVerifyConfig(o *flags.AddImageOpts) cosign.Config {
switch {
case o.Key != "":
return cosign.Config{Key: o.Key, Tlog: o.Tlog, InsecureSkipTLSVerify: derefInsecure(o.InsecureSkipTLSVerify), CaFile: o.CaFile}
return cosign.Config{Key: o.Key, Tlog: o.Tlog, InsecureSkipTLSVerify: o.InsecureSkipTLSVerify, CaFile: o.CaFile}
case o.CertIdentityRegexp != "" || o.CertIdentity != "":
return cosign.Config{
CertIdentity: o.CertIdentity,
@@ -256,7 +256,7 @@ func addImageVerifyConfig(o *flags.AddImageOpts) cosign.Config {
CertOidcIssuer: o.CertOidcIssuer,
CertOidcIssuerRegexp: o.CertOidcIssuerRegexp,
CertGithubWorkflowRepository: o.CertGithubWorkflowRepository,
InsecureSkipTLSVerify: derefInsecure(o.InsecureSkipTLSVerify),
InsecureSkipTLSVerify: o.InsecureSkipTLSVerify,
CaFile: o.CaFile,
}
default:
@@ -480,7 +480,7 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin
return err
}
insecureSkipTLSVerify := derefInsecure(i.InsecureSkipTLSVerify)
insecureSkipTLSVerify := i.InsecureSkipTLSVerify
caFile := i.CaFile
log.BaseFromContext(ctx).Debugf("resolving image [%s] (verified=%t, platform=%q, excludeExtras=%t, insecureSkipTLSVerify=%t, caFile=%q, rewrite=%q, digest=%q)", i.Name, verified, platform, excludeExtras, insecureSkipTLSVerify, caFile, rewrite, pinnedDigest)
@@ -619,7 +619,13 @@ func rewriteReference(ctx context.Context, s *store.Layout, oldRef name.Referenc
// index.docker.io. Preserve the original registry when the source is non-docker.
if newRegistry == "index.docker.io" && !strings.HasPrefix(rawRewrite, "docker.io") && !strings.HasPrefix(rawRewrite, "index.docker.io") {
newRegistry = oldRegistry
newRepo = strings.TrimPrefix(newRepo, "library/") //if rewrite has library/ prefix in path it is stripped off unless registry specified in rewrite
rewriteRepo := strings.TrimPrefix(rawRewrite, "/")
if i := strings.LastIndex(rewriteRepo, ":"); i != -1 {
rewriteRepo = rewriteRepo[:i]
}
if !strings.HasPrefix(rewriteRepo, "library/") {
newRepo = strings.TrimPrefix(newRepo, "library/")
}
}
oldTotal := oldRepo + ":" + oldTag
newTotal := newRepo + ":" + newTag
@@ -777,11 +783,11 @@ type chartJob struct {
//
// The three precedence rules are not uniform. registry is CLI > annotation.
// excludeExtras is a one-way switch that any of the three sources can flip on
// and none can flip off. platform is per-chart > CLI > annotation: an explicit
// --platform is run-time intent and outranks manifest metadata. That last rule
// must stay identical to resolveImageJobs's, or a single `hauler store sync`
// run would pull a chart's discovered images for a different platform than the
// manifest's own Images section.
// and none can flip off, since a plain bool has no unset state. platform is
// CLI > per-chart > annotation. That last rule must stay identical to
// resolveImageJobs's, or a single `hauler store sync` run would pull a
// chart's discovered images for a different platform than the manifest's own
// Images section.
//
// Every job allocates its own *action.ChartPathOptions. flags.AddChartOpts
// holds that as a pointer, so copying the struct alone would leave sibling
@@ -795,20 +801,15 @@ func resolveChartJobs(o *flags.SyncOpts, annotations map[string]string, manifest
jobs := make([]chartJob, 0, len(charts))
for _, ch := range charts {
excludeExtras := o.ExcludeExtras
if !o.ExcludeExtras && annotations[consts.ImageAnnotationExcludeExtras] == "true" {
excludeExtras = true
}
if ch.ExcludeExtras {
excludeExtras = ch.ExcludeExtras
}
excludeExtras := resolveBoolFlag(ch.ExcludeExtras, annotations[consts.ImageAnnotationExcludeExtras] == "true", o.ExcludeExtras, o.ExcludeExtrasChanged)
platform := o.Platform
if o.Platform == "" && annotations[consts.ImageAnnotationPlatform] != "" {
platform = annotations[consts.ImageAnnotationPlatform]
}
if ch.Platform != "" {
platform = ch.Platform
if o.Platform == "" {
if ch.Platform != "" {
platform = ch.Platform
} else if annotations[consts.ImageAnnotationPlatform] != "" {
platform = annotations[consts.ImageAnnotationPlatform]
}
}
var valuesFiles []string
@@ -826,16 +827,13 @@ func resolveChartJobs(o *flags.SyncOpts, annotations map[string]string, manifest
if caFile == "" {
if ch.CaFile != "" {
caFile = ch.CaFile
} else if annotations[consts.ImageAnnotationCaFile] == "true" {
} else if annotations[consts.ImageAnnotationCaFile] != "" {
caFile = annotations[consts.ImageAnnotationCaFile]
}
}
insecureSkipTLSVerify := false
if o.CaFile == "" {
insecureSkipTLSVerify = resolveInsecure(ch.InsecureSkipTLSVerify, annotations, o.InsecureSkipTLSVerify)
} else {
}
// a CA file and skipping TLS verification are mutually exclusive: providing one forces verification on
insecureSkipTLSVerify := o.CaFile == "" && resolveBoolFlag(ch.InsecureSkipTLSVerify, annotations[consts.ImageAnnotationInsecureSkipTLSVerify] == "true", o.InsecureSkipTLSVerify, o.InsecureChanged)
jobs = append(jobs, chartJob{
cfg: ch,
@@ -1381,12 +1379,11 @@ func fetchChart(ctx context.Context, s *store.Layout, j chartJob, tempRoot strin
// there is no separate per-discovered-image TLS knob in a chart
// manifest, so the registry a chart's images live in is assumed
// to share the chart repo's trust configuration.
chartInsecure := j.opts.ChartOpts.InsecureSkipTLSVerify
imageJobs = append(imageJobs, imageJob{
img: v1.Image{
Name: relocated,
CaFile: j.opts.ChartOpts.CaFile,
InsecureSkipTLSVerify: &chartInsecure,
InsecureSkipTLSVerify: j.opts.ChartOpts.InsecureSkipTLSVerify,
},
platform: j.opts.Platform,
excludeExtras: j.opts.ExcludeExtras,
@@ -1461,6 +1458,7 @@ func fetchChart(ctx context.Context, s *store.Layout, j chartJob, tempRoot strin
// rewrite. A rewrite that omits a tag inherits ref's.
func rewriteChartReference(ctx context.Context, s *store.Layout, ref name.Reference, rewrite string) error {
rewrite = strings.TrimPrefix(rewrite, "/")
rawRewrite := rewrite
newRef, err := name.ParseReference(rewrite)
if err != nil {
// error... don't continue with a bad reference
@@ -1483,6 +1481,13 @@ func rewriteChartReference(ctx context.Context, s *store.Layout, ref name.Refere
// rename chart name in store
oldRepo := ref.Context().RepositoryStr()
newRepo := newRef.Context().RepositoryStr()
rewriteRepo := rawRewrite
if i := strings.LastIndex(rewriteRepo, ":"); i != -1 {
rewriteRepo = rewriteRepo[:i]
}
if !strings.HasPrefix(rewriteRepo, "library/") {
newRepo = strings.TrimPrefix(newRepo, "library/")
}
newTag := newRef.Identifier()
if tag, ok := newRef.(name.Tag); ok {
newTag = tag.TagStr()
+167 -15
View File
@@ -375,6 +375,108 @@ func TestRewriteReference(t *testing.T) {
// condition fires → registry reverts to host, no library/ to strip
assertAnnotationsInStore(t, s, "newrepo/img:v2", host+"/newrepo/img:v2")
})
// The library/-detection must look at rawRewrite's path (not the
// go-containerregistry-normalized newRepo, which always carries "library/" for
// single-segment repos), so that a rewrite which explicitly asks for
// "library/..." is honored instead of being unconditionally stripped.
t.Run("path-only rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/nginx:latest",
consts.ContainerdImageNameKey: "index.docker.io/library/nginx:latest",
})
oldRef, _ := name.NewTag("nginx:latest")
newRef, _ := name.NewTag("library/nginx:v2")
rawRewrite := "library/nginx:v2"
if err := rewriteReference(ctx, s, oldRef, newRef, rawRewrite); err != nil {
t.Fatalf("rewriteReference: %v", err)
}
// rewriteRepo (derived from rawRewrite) starts with "library/" → must be kept
assertAnnotationsInStore(t, s, "library/nginx:v2", "index.docker.io/library/nginx:v2")
})
t.Run("leading slash rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/nginx:latest",
consts.ContainerdImageNameKey: "index.docker.io/library/nginx:latest",
})
oldRef, _ := name.NewTag("nginx:latest")
newRef, _ := name.NewTag("library/nginx:v2")
// AddImageCmd passes the pre-trim rewrite string through as rawRewrite, so a
// leading "/" must still be handled correctly here.
rawRewrite := "/library/nginx:v2"
if err := rewriteReference(ctx, s, oldRef, newRef, rawRewrite); err != nil {
t.Fatalf("rewriteReference: %v", err)
}
assertAnnotationsInStore(t, s, "library/nginx:v2", "index.docker.io/library/nginx:v2")
})
}
func TestRewriteChartReference(t *testing.T) {
ctx := newTestContext(t)
// A chart rewritten to a bare single-segment name must not keep an erroneous
// "library/" prefix picked up from go-containerregistry's docker hub
// normalization, unless the rewrite explicitly asked for one.
t.Run("path-only rewrite strips library/ prefix from docker hub normalization", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/mychart:1.0.0",
})
ref, _ := name.NewTag("mychart:1.0.0")
if err := rewriteChartReference(ctx, s, ref, "mychart:2.0.0"); err != nil {
t.Fatalf("rewriteChartReference: %v", err)
}
assertArtifactInStore(t, s, "mychart:2.0.0")
})
t.Run("explicit library/ prefix in rewrite is preserved", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/mychart:1.0.0",
})
ref, _ := name.NewTag("mychart:1.0.0")
if err := rewriteChartReference(ctx, s, ref, "library/mychart:2.0.0"); err != nil {
t.Fatalf("rewriteChartReference: %v", err)
}
assertArtifactInStore(t, s, "library/mychart:2.0.0")
})
t.Run("leading slash rewrite with explicit library/ prefix is preserved", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/mychart:1.0.0",
})
ref, _ := name.NewTag("mychart:1.0.0")
if err := rewriteChartReference(ctx, s, ref, "/library/mychart:2.0.0"); err != nil {
t.Fatalf("rewriteChartReference: %v", err)
}
assertArtifactInStore(t, s, "library/mychart:2.0.0")
})
t.Run("rewrite omitting tag inherits the source tag", func(t *testing.T) {
s := newTestStore(t)
seedStoreDescriptor(t, s, map[string]string{
ocispec.AnnotationRefName: "library/mychart:1.0.0",
})
ref, _ := name.NewTag("mychart:1.0.0")
if err := rewriteChartReference(ctx, s, ref, "myneworg/mychart"); err != nil {
t.Fatalf("rewriteChartReference: %v", err)
}
assertArtifactInStore(t, s, "myneworg/mychart:1.0.0")
})
}
// --------------------------------------------------------------------------
@@ -1798,12 +1900,13 @@ func TestResolveChartJobs_ExcludeExtras(t *testing.T) {
tests := []struct {
name string
cli bool
cliChanged bool
annotation string
perChart bool
want bool
}{
{name: "nothing set", want: false},
{name: "CLI flag alone", cli: true, want: true},
{name: "CLI flag alone", cli: true, cliChanged: true, want: true},
{name: "annotation alone", annotation: "true", want: true},
{name: "per-chart alone", perChart: true, want: true},
{
@@ -1816,14 +1919,16 @@ func TestResolveChartJobs_ExcludeExtras(t *testing.T) {
// --exclude-extras back off; both are one-way switches.
name: "CLI flag survives an annotation that is not true",
cli: true,
cliChanged: true,
annotation: "false",
want: true,
},
{
name: "CLI flag survives a false per-chart field",
cli: true,
perChart: false,
want: true,
name: "CLI flag survives a false per-chart field",
cli: true,
cliChanged: true,
perChart: false,
want: true,
},
{
name: "annotation survives a false per-chart field",
@@ -1831,11 +1936,20 @@ func TestResolveChartJobs_ExcludeExtras(t *testing.T) {
perChart: false,
want: true,
},
{
// An explicit CLI --exclude-extras=false wins outright over an
// annotation/per-chart true.
name: "explicit CLI false overrides annotation and per-chart",
cliChanged: true,
annotation: "true",
perChart: true,
want: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
o := &flags.SyncOpts{ExcludeExtras: tc.cli}
o := &flags.SyncOpts{ExcludeExtras: tc.cli, ExcludeExtrasChanged: tc.cliChanged}
a := map[string]string{}
if tc.annotation != "" {
a[consts.ImageAnnotationExcludeExtras] = tc.annotation
@@ -1877,10 +1991,16 @@ func TestResolveChartJobs_Platform(t *testing.T) {
want: "linux/amd64",
},
{
name: "per-chart wins over both",
name: "CLI flag wins over annotation and per-chart",
cli: "linux/amd64",
annotation: "linux/arm64",
perChart: "linux/s390x",
want: "linux/amd64",
},
{
name: "per-chart wins over annotation when CLI flag unset",
annotation: "linux/arm64",
perChart: "linux/s390x",
want: "linux/s390x",
},
{
@@ -2155,7 +2275,6 @@ func TestResolveChartJobs_NoCharts(t *testing.T) {
// TestResolveChartJobs_CredentialFields pins that every TLS/verification
// field on v1.Chart reaches the job's ChartOpts unchanged.
func TestResolveChartJobs_CredentialFields(t *testing.T) {
insecure := true
ch := v1.Chart{
Name: "rancher",
Verify: true,
@@ -2164,7 +2283,7 @@ func TestResolveChartJobs_CredentialFields(t *testing.T) {
CertFile: "/certs/client.crt",
KeyFile: "/certs/client.key",
CaFile: "/certs/ca.crt",
InsecureSkipTLSVerify: &insecure,
InsecureSkipTLSVerify: true,
PlainHTTP: true,
}
@@ -2195,14 +2314,47 @@ func TestResolveChartJobs_CredentialFields(t *testing.T) {
if opts.CaFile != ch.CaFile {
t.Errorf("CaFile = %q, want %q", opts.CaFile, ch.CaFile)
}
if opts.InsecureSkipTLSVerify != derefInsecure(ch.InsecureSkipTLSVerify) {
t.Errorf("InsecureSkipTLSVerify = %v, want %v", opts.InsecureSkipTLSVerify, derefInsecure(ch.InsecureSkipTLSVerify))
if opts.InsecureSkipTLSVerify != ch.InsecureSkipTLSVerify {
t.Errorf("InsecureSkipTLSVerify = %v, want %v", opts.InsecureSkipTLSVerify, ch.InsecureSkipTLSVerify)
}
if opts.PlainHTTP != ch.PlainHTTP {
t.Errorf("PlainHTTP = %v, want %v", opts.PlainHTTP, ch.PlainHTTP)
}
}
func TestResolveChartJobs_CaFilePrecedence(t *testing.T) {
tests := []struct {
name string
cli string
annotation string
perChart string
want string
}{
{name: "annotation used when CLI and per-chart unset", annotation: "/ann/ca.crt", want: "/ann/ca.crt"},
{name: "per-chart wins over annotation", annotation: "/ann/ca.crt", perChart: "/chart/ca.crt", want: "/chart/ca.crt"},
{name: "CLI wins over per-chart and annotation", cli: "/cli/ca.crt", annotation: "/ann/ca.crt", perChart: "/chart/ca.crt", want: "/cli/ca.crt"},
{name: "none set stays empty", want: ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
o := &flags.SyncOpts{CaFile: tc.cli}
a := map[string]string{}
if tc.annotation != "" {
a[consts.ImageAnnotationCaFile] = tc.annotation
}
jobs, err := resolveChartJobs(o, a, "/manifests", []v1.Chart{{Name: "rancher", CaFile: tc.perChart}})
if err != nil {
t.Fatalf("resolveChartJobs: %v", err)
}
if got := jobs[0].opts.ChartOpts.CaFile; got != tc.want {
t.Errorf("CaFile = %q, want %q", got, tc.want)
}
})
}
}
// TestResolveChartJobs_CredentialEnv pins that UsernameEnv/PasswordEnv are
// resolved into ChartOpts.Username/Password via resolveChartCreds.
func TestResolveChartJobs_CredentialEnv(t *testing.T) {
@@ -2999,7 +3151,7 @@ func TestStoreImage_CAFileAndInsecure(t *testing.T) {
t.Run("bad caFile without insecure returns error and stores nothing", func(t *testing.T) {
s := newTestStore(t)
insecure := false
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: &insecure}
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: insecure}
err := storeImage(ctx, s, img, "", false,
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
if err == nil {
@@ -3017,7 +3169,7 @@ func TestStoreImage_CAFileAndInsecure(t *testing.T) {
t.Fatal(err)
}
insecure := false
img := v1.Image{Name: ref, CaFile: junk, InsecureSkipTLSVerify: &insecure}
img := v1.Image{Name: ref, CaFile: junk, InsecureSkipTLSVerify: insecure}
err := storeImage(ctx, s, img, "", false,
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
if err == nil {
@@ -3031,7 +3183,7 @@ func TestStoreImage_CAFileAndInsecure(t *testing.T) {
// ignored and the pull still succeeds. If caFile were read first, the
// pull would error and nothing would be stored.
insecure := true
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: &insecure}
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: insecure}
err := storeImage(ctx, s, img, "", false,
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
if err != nil {
@@ -3043,7 +3195,7 @@ func TestStoreImage_CAFileAndInsecure(t *testing.T) {
t.Run("valid caFile without insecure is accepted", func(t *testing.T) {
s := newTestStore(t)
insecure := false
img := v1.Image{Name: ref, CaFile: writeCAFile(t), InsecureSkipTLSVerify: &insecure}
img := v1.Image{Name: ref, CaFile: writeCAFile(t), InsecureSkipTLSVerify: insecure}
err := storeImage(ctx, s, img, "", false,
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
if err != nil {
+24 -12
View File
@@ -562,6 +562,26 @@ func newItemWithDigest(s *store.Layout, digestStr string, desc ocispec.Descripto
return item
}
// resolveDisplayReference returns the fully-qualified reference string to display
// for desc. ContainerdImageNameKey already holds the canonical "registry/repo:tag"
// string exactly as computed when the artifact was stored (see rewriteReference in
// cmd/hauler/cli/store/add.go), so it's used verbatim. Re-parsing it through
// name.ParseReference and calling .Name() would re-trigger go-containerregistry's
// Docker Hub "library/" namespace normalization for any single-segment repo under
// index.docker.io, undoing a rewrite like "hello-world-custom" back to
// "library/hello-world-custom". AnnotationRefName, used as a fallback, has no
// registry component, so it still needs reference.Parse to fill one in.
func resolveDisplayReference(desc ocispec.Descriptor) (string, error) {
if refName := desc.Annotations[consts.ContainerdImageNameKey]; refName != "" {
return refName, nil
}
ref, err := reference.Parse(desc.Annotations[ocispec.AnnotationRefName])
if err != nil {
return "", err
}
return ref.Name(), nil
}
func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat string, o *flags.InfoOpts) item {
var size int64 = 0
for _, l := range m.Layers {
@@ -570,11 +590,7 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
ctype := resolveCtype(desc, m.Config.MediaType)
refName := desc.Annotations[consts.ContainerdImageNameKey]
if refName == "" {
refName = desc.Annotations[ocispec.AnnotationRefName]
}
ref, err := reference.Parse(refName)
refName, err := resolveDisplayReference(desc)
if err != nil {
return item{}
}
@@ -584,7 +600,7 @@ func newItem(s *store.Layout, desc ocispec.Descriptor, m ocispec.Manifest, plat
}
return item{
Reference: ref.Name(),
Reference: refName,
Type: ctype,
Platform: plat,
Digest: desc.Digest.String(),
@@ -637,17 +653,13 @@ func fallbackItem(desc ocispec.Descriptor, plat string, problem store.BlobResult
plat = "-"
}
refName := desc.Annotations[consts.ContainerdImageNameKey]
if refName == "" {
refName = desc.Annotations[ocispec.AnnotationRefName]
}
ref, err := reference.Parse(refName)
refName, err := resolveDisplayReference(desc)
if err != nil {
return item{}
}
return item{
Reference: ref.Name(),
Reference: refName,
Type: resolveCtype(desc, ""),
Platform: plat,
Layers: 0,
+76
View File
@@ -215,6 +215,82 @@ func TestNewItem(t *testing.T) {
}
}
func TestResolveDisplayReference(t *testing.T) {
// ContainerdImageNameKey already holds the fully-qualified reference exactly as
// computed by rewriteReference (see add.go), so it must be returned verbatim.
// Re-parsing it through the reference package would re-trigger
// go-containerregistry's docker hub "library/" normalization for a
// single-segment repo, undoing a rewrite like "hello-world-custom" back to
// "library/hello-world-custom".
t.Run("ContainerdImageNameKey is used verbatim, without re-injecting library/", func(t *testing.T) {
desc := ocispec.Descriptor{
Annotations: map[string]string{
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
ocispec.AnnotationRefName: "hello-world-custom:v2",
},
}
got, err := resolveDisplayReference(desc)
if err != nil {
t.Fatalf("resolveDisplayReference: %v", err)
}
if want := "index.docker.io/hello-world-custom:v2"; got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("falls back to AnnotationRefName parsed when ContainerdImageNameKey absent", func(t *testing.T) {
desc := ocispec.Descriptor{
Annotations: map[string]string{
ocispec.AnnotationRefName: "hello-world-custom:v2",
},
}
got, err := resolveDisplayReference(desc)
if err != nil {
t.Fatalf("resolveDisplayReference: %v", err)
}
if want := "hauler/hello-world-custom:v2"; got != want {
t.Errorf("got %q, want %q", got, want)
}
})
t.Run("returns error when fallback ref cannot be parsed", func(t *testing.T) {
desc := ocispec.Descriptor{Annotations: map[string]string{}}
if _, err := resolveDisplayReference(desc); err == nil {
t.Fatal("expected error, got nil")
}
})
}
func TestNewItem_ReferenceUsesContainerdImageNameVerbatim(t *testing.T) {
desc := ocispec.Descriptor{
Annotations: map[string]string{
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
ocispec.AnnotationRefName: "hello-world-custom:v2",
},
}
m := ocispec.Manifest{Config: ocispec.Descriptor{MediaType: consts.DockerConfigJSON}}
o := &flags.InfoOpts{TypeFilter: "all"}
got := newItem(nil, desc, m, "linux/amd64", o)
if want := "index.docker.io/hello-world-custom:v2"; got.Reference != want {
t.Errorf("got Reference %q, want %q", got.Reference, want)
}
}
func TestFallbackItem_ReferenceUsesContainerdImageNameVerbatim(t *testing.T) {
desc := ocispec.Descriptor{
Annotations: map[string]string{
consts.ContainerdImageNameKey: "index.docker.io/hello-world-custom:v2",
ocispec.AnnotationRefName: "hello-world-custom:v2",
},
}
got := fallbackItem(desc, "linux/amd64", store.BlobResult{})
if want := "index.docker.io/hello-world-custom:v2"; got.Reference != want {
t.Errorf("got Reference %q, want %q", got.Reference, want)
}
}
func TestInfoCmd(t *testing.T) {
ctx := newTestContext(t)
s := newTestStore(t)
+97 -107
View File
@@ -189,7 +189,7 @@ func SyncCmd(ctx context.Context, o *flags.SyncOpts, s *store.Layout, rso *flags
if strings.HasPrefix(haulPath, "http://") || strings.HasPrefix(haulPath, "https://") {
l.Debugf("detected remote manifest... starting download... [%s]", haulPath)
h := getter.NewHttp(derefInsecure(o.InsecureSkipTLSVerify), o.CaFile)
h := getter.NewHttp(o.InsecureSkipTLSVerify, o.CaFile)
parsedURL, err := url.Parse(haulPath)
if err != nil {
return err
@@ -241,7 +241,7 @@ func SyncCmd(ctx context.Context, o *flags.SyncOpts, s *store.Layout, rso *flags
if strings.HasPrefix(haulPath, "http://") || strings.HasPrefix(haulPath, "https://") {
l.Debugf("detected remote image.txt... starting download... [%s]", haulPath)
h := getter.NewHttp(derefInsecure(o.InsecureSkipTLSVerify), o.CaFile)
h := getter.NewHttp(o.InsecureSkipTLSVerify, o.CaFile)
parsedURL, err := url.Parse(haulPath)
if err != nil {
return err
@@ -287,28 +287,16 @@ func SyncCmd(ctx context.Context, o *flags.SyncOpts, s *store.Layout, rso *flags
return nil
}
// resolveInsecure applies precedence: cli > per-item > annotation.
// A non-nil per-item pointer wins outright — including an explicit false — so an
// individual file/image/chart can opt out of an insecure annotation or the global
// --insecure-skip-tls-verify flag. nil means "not set on the item", which falls
// through to the annotation, then the global flag.
func resolveInsecure(item *bool, ann map[string]string, global *bool) bool {
if global != nil {
return *global
// resolveBoolFlag applies CLI-first precedence for a plain-bool flag: an
// explicitly-set CLI flag wins outright (even when false); otherwise the flag
// is on if the resolved CLI value (e.g. from an env var), the per-item field,
// or the annotation is true. Plain bools have no unset state, so per-item and
// annotation can only turn a flag on, never force it back off.
func resolveBoolFlag(item, annTrue, global, cliChanged bool) bool {
if cliChanged {
return global
}
if item != nil {
return *item
}
if ann != nil && ann[consts.ImageAnnotationInsecureSkipTLSVerify] == "true" {
return true
}
return false
}
// derefInsecure is a nil-safe read of a *bool for logging/plumbing where a plain
// bool is needed. nil reads as false.
func derefInsecure(p *bool) bool {
return p != nil && *p
return global || item || annTrue
}
func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, targetStores map[string]*store.Layout) error {
@@ -347,11 +335,11 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
return err
}
a := cfg.GetAnnotations()
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores)
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores, o.StoreChanged)
if err != nil {
return err
}
docRso, err := resolveDocRetries(a, rso)
docRso, err := resolveDocRetries(a, rso, o.RetriesChanged)
if err != nil {
return err
}
@@ -374,11 +362,11 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
}
a := cfg.GetAnnotations()
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores)
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores, o.StoreChanged)
if err != nil {
return err
}
docRso, err := resolveDocRetries(a, rso)
docRso, err := resolveDocRetries(a, rso, o.RetriesChanged)
if err != nil {
return err
}
@@ -403,11 +391,11 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
return err
}
a := cfg.GetAnnotations()
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores)
docStore, err := resolveTargetStore(ctx, a, s, rso, ro, targetStores, o.StoreChanged)
if err != nil {
return err
}
docRso, err := resolveDocRetries(a, rso)
docRso, err := resolveDocRetries(a, rso, o.RetriesChanged)
if err != nil {
return err
}
@@ -433,7 +421,12 @@ func processContent(ctx context.Context, fi *os.File, o *flags.SyncOpts, s *stor
// resolveTargetStore picks a doc's store based on its hauler.dev/store annotation,
// falling back to def. Opens (or reuses, via targetStores) the target store otherwise.
func resolveTargetStore(ctx context.Context, a map[string]string, def *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, targetStores map[string]*store.Layout) (*store.Layout, error) {
// An explicit CLI --store wins: cliStoreSet short-circuits to def, ignoring the annotation.
func resolveTargetStore(ctx context.Context, a map[string]string, def *store.Layout, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, targetStores map[string]*store.Layout, cliStoreSet bool) (*store.Layout, error) {
if cliStoreSet {
return def, nil
}
target := a[consts.AnnotationTargetStore]
if target == "" {
return def, nil
@@ -466,8 +459,13 @@ func resolveTargetStore(ctx context.Context, a map[string]string, def *store.Lay
// resolveDocRetries returns a copy of rso with Retries overridden by a doc's
// hauler.dev/retries annotation, or rso unchanged if it's not set. Copy, not
// mutation, so it can't leak into a sibling doc.
func resolveDocRetries(a map[string]string, rso *flags.StoreRootOpts) (*flags.StoreRootOpts, error) {
// mutation, so it can't leak into a sibling doc. An explicit CLI --retries
// wins: cliRetriesSet returns rso unchanged, ignoring the annotation.
func resolveDocRetries(a map[string]string, rso *flags.StoreRootOpts, cliRetriesSet bool) (*flags.StoreRootOpts, error) {
if cliRetriesSet {
return rso, nil
}
v, ok := a[consts.AnnotationRetries]
if !ok || v == "" {
return rso, nil
@@ -604,10 +602,12 @@ type imageJob struct {
certGithubWorkflowRepository string
}
// resolveImageJobs applies the precedence rules (per-image > annotation >
// CLI, except registry relocation which is CLI > annotation) to every image
// in images, producing one imageJob per image. It is pure -- cosign
// verification happens later, inside the pull worker; see resolveAndVerify.
// resolveImageJobs applies the precedence rules (CLI > per-image > annotation)
// to every image in images, producing one imageJob per image. For the boolean
// flags (tlog, exclude-extras, insecure-skip-tls-verify) an explicit CLI flag
// wins outright; otherwise per-image or annotation can only turn them on (see
// resolveBoolFlag). It is pure -- cosign verification happens later, inside the
// pull worker; see resolveAndVerify.
func resolveImageJobs(o *flags.SyncOpts, a map[string]string, images []v1.Image) ([]imageJob, error) {
var jobs []imageJob
@@ -635,11 +635,8 @@ func resolveImageJobs(o *flags.SyncOpts, a map[string]string, images []v1.Image)
i.CaFile = o.CaFile
}
insecureSkipTLSVerify := false
if o.CaFile == "" {
insecureSkipTLSVerify = resolveInsecure(i.InsecureSkipTLSVerify, a, o.InsecureSkipTLSVerify)
}
i.InsecureSkipTLSVerify = &insecureSkipTLSVerify
// a CA file and skipping TLS verification are mutually exclusive: providing one forces verification on
i.InsecureSkipTLSVerify = o.CaFile == "" && resolveBoolFlag(i.InsecureSkipTLSVerify, a[consts.ImageAnnotationInsecureSkipTLSVerify] == "true", o.InsecureSkipTLSVerify, o.InsecureChanged)
if i.Local {
needsPubKeyVerification := a[consts.ImageAnnotationKey] != "" || o.Key != "" || i.Key != ""
@@ -669,71 +666,71 @@ func resolveImageJobs(o *flags.SyncOpts, a map[string]string, images []v1.Image)
if needsPubKeyVerification {
key := o.Key
if o.Key == "" && a[consts.ImageAnnotationKey] != "" {
expanded, err := homedir.Expand(a[consts.ImageAnnotationKey])
if err != nil {
return nil, err
if o.Key == "" {
if i.Key != "" {
expanded, err := homedir.Expand(i.Key)
if err != nil {
return nil, err
}
key = expanded
} else if a[consts.ImageAnnotationKey] != "" {
expanded, err := homedir.Expand(a[consts.ImageAnnotationKey])
if err != nil {
return nil, err
}
key = expanded
}
key = expanded
}
if i.Key != "" {
expanded, err := homedir.Expand(i.Key)
if err != nil {
return nil, err
}
key = expanded
}
tlog := o.Tlog
if !o.Tlog && a[consts.ImageAnnotationTlog] == "true" {
tlog = true
}
if i.Tlog {
tlog = i.Tlog
}
tlog := resolveBoolFlag(i.Tlog, a[consts.ImageAnnotationTlog] == "true", o.Tlog, o.TlogChanged)
job.needsPubKey = true
job.key = key
job.tlog = tlog
} else if needsKeylessVerificaton { //Keyless signature verification
certIdentityRegexp := o.CertIdentityRegexp
if o.CertIdentityRegexp == "" && a[consts.ImageAnnotationCertIdentityRegexp] != "" {
certIdentityRegexp = a[consts.ImageAnnotationCertIdentityRegexp]
}
if i.CertIdentityRegexp != "" {
certIdentityRegexp = i.CertIdentityRegexp
if o.CertIdentityRegexp == "" {
if i.CertIdentityRegexp != "" {
certIdentityRegexp = i.CertIdentityRegexp
} else if a[consts.ImageAnnotationCertIdentityRegexp] != "" {
certIdentityRegexp = a[consts.ImageAnnotationCertIdentityRegexp]
}
}
certIdentity := o.CertIdentity
if o.CertIdentity == "" && a[consts.ImageAnnotationCertIdentity] != "" {
certIdentity = a[consts.ImageAnnotationCertIdentity]
}
if i.CertIdentity != "" {
certIdentity = i.CertIdentity
if o.CertIdentity == "" {
if i.CertIdentity != "" {
certIdentity = i.CertIdentity
} else if a[consts.ImageAnnotationCertIdentity] != "" {
certIdentity = a[consts.ImageAnnotationCertIdentity]
}
}
certOidcIssuer := o.CertOidcIssuer
if o.CertOidcIssuer == "" && a[consts.ImageAnnotationCertOidcIssuer] != "" {
certOidcIssuer = a[consts.ImageAnnotationCertOidcIssuer]
}
if i.CertOidcIssuer != "" {
certOidcIssuer = i.CertOidcIssuer
if o.CertOidcIssuer == "" {
if i.CertOidcIssuer != "" {
certOidcIssuer = i.CertOidcIssuer
} else if a[consts.ImageAnnotationCertOidcIssuer] != "" {
certOidcIssuer = a[consts.ImageAnnotationCertOidcIssuer]
}
}
certOidcIssuerRegexp := o.CertOidcIssuerRegexp
if o.CertOidcIssuerRegexp == "" && a[consts.ImageAnnotationCertOidcIssuerRegexp] != "" {
certOidcIssuerRegexp = a[consts.ImageAnnotationCertOidcIssuerRegexp]
}
if i.CertOidcIssuerRegexp != "" {
certOidcIssuerRegexp = i.CertOidcIssuerRegexp
if o.CertOidcIssuerRegexp == "" {
if i.CertOidcIssuerRegexp != "" {
certOidcIssuerRegexp = i.CertOidcIssuerRegexp
} else if a[consts.ImageAnnotationCertOidcIssuerRegexp] != "" {
certOidcIssuerRegexp = a[consts.ImageAnnotationCertOidcIssuerRegexp]
}
}
certGithubWorkflowRepository := o.CertGithubWorkflowRepository
if o.CertGithubWorkflowRepository == "" && a[consts.ImageAnnotationCertGithubWorkflowRepository] != "" {
certGithubWorkflowRepository = a[consts.ImageAnnotationCertGithubWorkflowRepository]
}
if i.CertGithubWorkflowRepository != "" {
certGithubWorkflowRepository = i.CertGithubWorkflowRepository
if o.CertGithubWorkflowRepository == "" {
if i.CertGithubWorkflowRepository != "" {
certGithubWorkflowRepository = i.CertGithubWorkflowRepository
} else if a[consts.ImageAnnotationCertGithubWorkflowRepository] != "" {
certGithubWorkflowRepository = a[consts.ImageAnnotationCertGithubWorkflowRepository]
}
}
job.needsKeyless = true
@@ -745,11 +742,12 @@ func resolveImageJobs(o *flags.SyncOpts, a map[string]string, images []v1.Image)
}
platform := o.Platform
if o.Platform == "" && a[consts.ImageAnnotationPlatform] != "" {
platform = a[consts.ImageAnnotationPlatform]
}
if i.Platform != "" {
platform = i.Platform
if o.Platform == "" {
if i.Platform != "" {
platform = i.Platform
} else if a[consts.ImageAnnotationPlatform] != "" {
platform = a[consts.ImageAnnotationPlatform]
}
}
rewrite := ""
@@ -757,13 +755,7 @@ func resolveImageJobs(o *flags.SyncOpts, a map[string]string, images []v1.Image)
rewrite = i.Rewrite
}
excludeExtras := o.ExcludeExtras
if !o.ExcludeExtras && a[consts.ImageAnnotationExcludeExtras] == "true" {
excludeExtras = true
}
if i.ExcludeExtras {
excludeExtras = i.ExcludeExtras
}
excludeExtras := resolveBoolFlag(i.ExcludeExtras, a[consts.ImageAnnotationExcludeExtras] == "true", o.ExcludeExtras, o.ExcludeExtrasChanged)
job.platform = platform
job.rewrite = rewrite
@@ -795,7 +787,7 @@ func (j imageJob) verifyConfig() cosign.Config {
return cosign.Config{
Key: j.key,
Tlog: j.tlog,
InsecureSkipTLSVerify: derefInsecure(j.img.InsecureSkipTLSVerify),
InsecureSkipTLSVerify: j.img.InsecureSkipTLSVerify,
CaFile: j.img.CaFile,
}
case j.needsKeyless:
@@ -805,7 +797,7 @@ func (j imageJob) verifyConfig() cosign.Config {
CertOidcIssuer: j.certOidcIssuer,
CertOidcIssuerRegexp: j.certOidcIssuerRegexp,
CertGithubWorkflowRepository: j.certGithubWorkflowRepository,
InsecureSkipTLSVerify: derefInsecure(j.img.InsecureSkipTLSVerify),
InsecureSkipTLSVerify: j.img.InsecureSkipTLSVerify,
CaFile: j.img.CaFile,
}
default:
@@ -1152,9 +1144,10 @@ type fileJob struct {
file v1.File
}
// resolveFileJobs converts every v1.File in files into a fileJob, applying
// the caFile/insecure precedence rules (per-file > annotation > global) --
// see resolveInsecure. It is pure.
// resolveFileJobs converts every v1.File in files into a fileJob. caFile is
// CLI > per-file > annotation; insecureSkipTLSVerify is CLI-first (an explicit
// CLI flag wins, otherwise per-file or annotation can only turn it on -- see
// resolveBoolFlag). It is pure.
func resolveFileJobs(o *flags.SyncOpts, a map[string]string, files []v1.File) []fileJob {
jobs := make([]fileJob, 0, len(files))
for _, f := range files {
@@ -1166,11 +1159,8 @@ func resolveFileJobs(o *flags.SyncOpts, a map[string]string, files []v1.File) []
f.CaFile = o.CaFile
}
insecure := false
if o.CaFile == "" {
insecure = resolveInsecure(f.InsecureSkipTLSVerify, a, o.InsecureSkipTLSVerify)
}
f.InsecureSkipTLSVerify = &insecure
// a CA file and skipping TLS verification are mutually exclusive: providing one forces verification on
f.InsecureSkipTLSVerify = o.CaFile == "" && resolveBoolFlag(f.InsecureSkipTLSVerify, a[consts.ImageAnnotationInsecureSkipTLSVerify] == "true", o.InsecureSkipTLSVerify, o.InsecureChanged)
jobs = append(jobs, fileJob{file: f})
}
+95 -17
View File
@@ -245,7 +245,7 @@ func TestResolveDocRetries_NoAnnotation_ReturnsRsoUnchanged(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
rso.Retries = 5
got, err := resolveDocRetries(map[string]string{}, rso)
got, err := resolveDocRetries(map[string]string{}, rso, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -258,7 +258,7 @@ func TestResolveDocRetries_Override_ReturnsCopyNotMutation(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
rso.Retries = 5
got, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "9"}, rso)
got, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "9"}, rso, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -273,11 +273,27 @@ func TestResolveDocRetries_Override_ReturnsCopyNotMutation(t *testing.T) {
}
}
func TestResolveDocRetries_CLIWinsOverAnnotation(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
rso.Retries = 5
got, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "9"}, rso, true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != rso {
t.Fatal("expected rso unchanged when --retries was set on the CLI")
}
if got.Retries != 5 {
t.Fatalf("got Retries %d, want the CLI value 5", got.Retries)
}
}
func TestResolveDocRetries_ZeroMeansDefault(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
rso.Retries = 5
got, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "0"}, rso)
got, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "0"}, rso, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -289,7 +305,7 @@ func TestResolveDocRetries_ZeroMeansDefault(t *testing.T) {
func TestResolveDocRetries_Negative_ReturnsError(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
if _, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "-1"}, rso); err == nil {
if _, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "-1"}, rso, false); err == nil {
t.Fatal("expected an error for a negative hauler.dev/retries value, got nil")
}
}
@@ -297,7 +313,7 @@ func TestResolveDocRetries_Negative_ReturnsError(t *testing.T) {
func TestResolveDocRetries_NotANumber_ReturnsError(t *testing.T) {
rso := defaultRootOpts(t.TempDir())
if _, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "banana"}, rso); err == nil {
if _, err := resolveDocRetries(map[string]string{consts.AnnotationRetries: "banana"}, rso, false); err == nil {
t.Fatal("expected an error for a non-numeric hauler.dev/retries value, got nil")
}
}
@@ -943,10 +959,17 @@ func TestResolveImageJobs_KeyPrecedence(t *testing.T) {
wantKey: homeKey,
},
{
name: "per-image overrides annotation and CLI, expanded via homedir",
// CLI wins outright over both annotation and per-image.
name: "CLI overrides annotation and per-image",
cliKey: "/cli/key.pub",
annotation: "/annotation/key.pub",
imageKey: "~/mykey.pub",
wantKey: "/cli/key.pub",
},
{
name: "per-image overrides annotation when CLI key unset, expanded via homedir",
annotation: "/annotation/key.pub",
imageKey: "~/mykey.pub",
wantKey: homeKey,
},
}
@@ -982,25 +1005,36 @@ func TestResolveImageJobs_TlogPrecedence(t *testing.T) {
tests := []struct {
name string
cliTlog bool
cliChanged bool
annotation string
imageTlog bool
wantTlog bool
}{
{
name: "CLI true",
cliTlog: true,
wantTlog: true,
name: "CLI true",
cliTlog: true,
cliChanged: true,
wantTlog: true,
},
{
name: "annotation true overrides CLI false",
name: "annotation true when CLI unset",
annotation: "true",
wantTlog: true,
},
{
name: "per-image true overrides annotation/CLI false",
name: "per-image true when CLI unset",
imageTlog: true,
wantTlog: true,
},
{
// An explicit CLI flag wins outright, even false, over an
// annotation/per-image true.
name: "explicit CLI false overrides annotation and per-image",
cliChanged: true,
annotation: "true",
imageTlog: true,
wantTlog: false,
},
{
name: "all false stays false",
wantTlog: false,
@@ -1009,7 +1043,7 @@ func TestResolveImageJobs_TlogPrecedence(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
o := &flags.SyncOpts{Key: "/cli/key.pub", Tlog: tc.cliTlog}
o := &flags.SyncOpts{Key: "/cli/key.pub", Tlog: tc.cliTlog, TlogChanged: tc.cliChanged}
a := map[string]string{}
if tc.annotation != "" {
a[consts.ImageAnnotationTlog] = tc.annotation
@@ -1042,7 +1076,9 @@ func TestResolveImageJobs_PlatformPrecedence(t *testing.T) {
// Annotation only applies when the CLI platform is unset — it does not
// override an explicitly-set CLI platform.
{name: "annotation used when CLI platform unset", annotation: "linux/arm64", want: "linux/arm64"},
{name: "per-image overrides annotation and CLI", cliPlatform: "linux/amd64", annotation: "linux/arm64", imagePlatform: "linux/386", want: "linux/386"},
// CLI wins outright over both annotation and per-image.
{name: "CLI overrides annotation and per-image", cliPlatform: "linux/amd64", annotation: "linux/arm64", imagePlatform: "linux/386", want: "linux/amd64"},
{name: "per-image overrides annotation when CLI platform unset", annotation: "linux/arm64", imagePlatform: "linux/386", want: "linux/386"},
{name: "none set stays empty", want: ""},
}
@@ -1103,19 +1139,21 @@ func TestResolveImageJobs_ExcludeExtrasPrecedence(t *testing.T) {
tests := []struct {
name string
cliExcludeExtras bool
cliChanged bool
annotation string
imageExclude bool
want bool
}{
{name: "CLI true", cliExcludeExtras: true, want: true},
{name: "annotation true overrides CLI false", annotation: "true", want: true},
{name: "per-image true overrides annotation/CLI false", imageExclude: true, want: true},
{name: "CLI true", cliExcludeExtras: true, cliChanged: true, want: true},
{name: "annotation true when CLI unset", annotation: "true", want: true},
{name: "per-image true when CLI unset", imageExclude: true, want: true},
{name: "explicit CLI false overrides annotation and per-image", cliChanged: true, annotation: "true", imageExclude: true, want: false},
{name: "all false stays false", want: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
o := &flags.SyncOpts{ExcludeExtras: tc.cliExcludeExtras}
o := &flags.SyncOpts{ExcludeExtras: tc.cliExcludeExtras, ExcludeExtrasChanged: tc.cliChanged}
a := map[string]string{}
if tc.annotation != "" {
a[consts.ImageAnnotationExcludeExtras] = tc.annotation
@@ -1136,6 +1174,46 @@ func TestResolveImageJobs_ExcludeExtrasPrecedence(t *testing.T) {
}
}
func TestResolveImageJobs_InsecurePrecedence(t *testing.T) {
tests := []struct {
name string
cliCaFile string
cliChanged bool
annotation string
imageIns bool
want bool
}{
{name: "per-image true when CLI unset", imageIns: true, want: true},
{name: "annotation true when CLI unset", annotation: "true", want: true},
{name: "explicit CLI false overrides annotation and per-image", cliChanged: true, annotation: "true", imageIns: true, want: false},
// a CA file forces verification on, overriding a per-image/annotation true
{name: "ca-file forces insecure off", cliCaFile: "/ca.pem", annotation: "true", imageIns: true, want: false},
{name: "all unset stays false", want: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
o := &flags.SyncOpts{CaFile: tc.cliCaFile, InsecureChanged: tc.cliChanged}
a := map[string]string{}
if tc.annotation != "" {
a[consts.ImageAnnotationInsecureSkipTLSVerify] = tc.annotation
}
images := []v1.Image{{Name: "rancher/rancher:v2.9", InsecureSkipTLSVerify: tc.imageIns}}
jobs, err := resolveImageJobs(o, a, images)
if err != nil {
t.Fatalf("resolveImageJobs: %v", err)
}
if len(jobs) != 1 {
t.Fatalf("expected 1 job, got %d", len(jobs))
}
if jobs[0].img.InsecureSkipTLSVerify != tc.want {
t.Errorf("got insecure %v, want %v", jobs[0].img.InsecureSkipTLSVerify, tc.want)
}
})
}
}
func TestResolveImageJobs_NoOptions_MinimalJob(t *testing.T) {
o := &flags.SyncOpts{}
images := []v1.Image{{Name: "rancher/rancher:v2.9"}}
+2 -2
View File
@@ -22,7 +22,7 @@ type AddImageOpts struct {
ExcludeExtras bool
Local bool
CaFile string
InsecureSkipTLSVerify *bool
InsecureSkipTLSVerify bool
}
func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
@@ -39,7 +39,7 @@ func (o *AddImageOpts) AddFlags(cmd *cobra.Command) {
f.BoolVar(&o.ExcludeExtras, "exclude-extras", false, "(Optional) Exclude cosign signatures, attestations, SBOMs, and OCI referrers when pulling the image")
f.BoolVar(&o.Local, "local", false, "(Optional) Add image from the local Docker daemon instead of a remote registry")
f.StringVar(&o.CaFile, "ca-file", "", "(Optional) Location of CA Bundle to enable certification verification")
f.Bool("insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification")
f.BoolVar(&o.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification")
}
type AddFileOpts struct {
+12 -2
View File
@@ -26,7 +26,17 @@ type SyncOpts struct {
Concurrency int
NoProgress bool
CaFile string
InsecureSkipTLSVerify *bool
InsecureSkipTLSVerify bool
// Whether each of these flags was explicitly set on the CLI, captured in
// sync's PreRunE. A plain bool (and a resolved store/retries value) has no
// "unset" state, so the resolvers use these markers to let an explicit CLI
// value win over per-item/annotation instead of only ever turning a flag on.
TlogChanged bool
ExcludeExtrasChanged bool
InsecureChanged bool
StoreChanged bool
RetriesChanged bool
}
func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
@@ -50,5 +60,5 @@ func (o *SyncOpts) AddFlags(cmd *cobra.Command) {
f.IntVarP(&o.Concurrency, "concurrency", "j", consts.DefaultConcurrency, "(Optional) Maximum number of artifacts to fetch and store concurrently (1 = serial; also via HAULER_CONCURRENCY, explicit flag wins)")
f.BoolVar(&o.NoProgress, "no-progress", false, "(Optional) Disable the live progress display")
f.StringVar(&o.CaFile, "ca-file", "", "(Optional) Location of CA Bundle to enable certification verification")
f.Bool("insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification")
f.BoolVar(&o.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "(Optional) Skip TLS certificate verification")
}
+1 -1
View File
@@ -41,6 +41,6 @@ type Chart struct {
CertFile string `json:"certFile,omitempty"`
KeyFile string `json:"keyFile,omitempty"`
CaFile string `json:"caFile,omitempty"`
InsecureSkipTLSVerify *bool `json:"insecureSkipTLSVerify,omitempty"`
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
PlainHTTP bool `json:"plainHTTP,omitempty"`
}
+1 -1
View File
@@ -26,5 +26,5 @@ type File struct {
// TLS options for verifying the file contents for remote files.
// If not specified, the default system CA bundle will be used.
CaFile string `json:"ca-file"`
InsecureSkipTLSVerify *bool `json:"insecure-skip-tls-verify,omitempty"`
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"`
}
+1 -1
View File
@@ -43,5 +43,5 @@ type Image struct {
// TLS options for verifying the image signature. If not specified, the default system CA bundle will be used.
CaFile string `json:"ca-file"`
InsecureSkipTLSVerify *bool `json:"insecure-skip-tls-verify,omitempty"`
InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify"`
}