Skip to content

Commit c6bb3f4

Browse files
authored
Merge branch 'main' into fixed-registry-options
2 parents ad999bc + 0a3d27b commit c6bb3f4

29 files changed

Lines changed: 571 additions & 94 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ vulncheck.out
2020
trivy.out
2121
CLAUDE.md
2222
**/CLAUDE.*
23-
.claude**
23+
.claude**
24+
__debug_bin*

cmd/hauler/cli/store.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package cli
22

33
import (
44
"fmt"
5+
"os"
6+
"strconv"
57

68
"github.com/spf13/cobra"
79
"helm.sh/helm/v4/pkg/action"
810

911
"hauler.dev/go/hauler/v2/cmd/hauler/cli/store"
1012
"hauler.dev/go/hauler/v2/internal/flags"
13+
"hauler.dev/go/hauler/v2/pkg/consts"
1114
"hauler.dev/go/hauler/v2/pkg/log"
1215
)
1316

@@ -71,6 +74,17 @@ func addStoreSync(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
7174
Short: "Sync content to the content store",
7275
Args: cobra.ExactArgs(0),
7376
PreRunE: func(cmd *cobra.Command, args []string) error {
77+
// Check for ca-file & insecure-skip-tls-verify env variables
78+
if o.CaFile == "" {
79+
o.CaFile = os.Getenv(consts.CaFile)
80+
}
81+
if o.InsecureSkipTLSVerify == nil {
82+
if v := os.Getenv(consts.InsecureSkipTLSVerify); v != "" {
83+
b, _ := strconv.ParseBool(v)
84+
o.InsecureSkipTLSVerify = &b
85+
}
86+
}
87+
7488
// --dry-run requires --products
7589
if o.DryRun && len(o.Products) == 0 {
7690
return fmt.Errorf("--dry-run requires --products")
@@ -110,6 +124,12 @@ func addStoreSync(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Comman
110124
}
111125
rso.BlobConcurrency = bc
112126

127+
// resolve *bool: nil unless the user explicitly passed the flag
128+
if cmd.Flags().Changed("insecure-skip-tls-verify") {
129+
v, _ := cmd.Flags().GetBool("insecure-skip-tls-verify")
130+
o.InsecureSkipTLSVerify = &v
131+
}
132+
113133
return nil
114134
},
115135
RunE: func(cmd *cobra.Command, args []string) error {
@@ -388,6 +408,19 @@ func addStoreAddImage(rso *flags.StoreRootOpts, ro *flags.CliRootOpts) *cobra.Co
388408
# add image from local Docker daemon
389409
hauler store add image my-local-app:latest --local`,
390410
Args: cobra.ExactArgs(1),
411+
PreRunE: func(cmd *cobra.Command, args []string) error {
412+
// Check for ca-file & insecure-skip-tls-verify env variables
413+
if o.CaFile == "" {
414+
o.CaFile = os.Getenv(consts.CaFile)
415+
}
416+
if o.InsecureSkipTLSVerify == nil {
417+
if v := os.Getenv(consts.InsecureSkipTLSVerify); v != "" {
418+
b, _ := strconv.ParseBool(v)
419+
o.InsecureSkipTLSVerify = &b
420+
}
421+
}
422+
return nil
423+
},
391424
RunE: func(cmd *cobra.Command, args []string) error {
392425
ctx := cmd.Context()
393426

cmd/hauler/cli/store/add.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ func AddFileCmd(ctx context.Context, o *flags.AddFileOpts, s *store.Layout, refe
5757
}()
5858

5959
cfg := v1.File{
60-
Path: reference,
60+
Path: reference,
61+
CaFile: o.CaFile,
62+
InsecureSkipTLSVerify: &o.InsecureSkipTLSVerify,
6163
}
6264
if len(o.Name) > 0 {
6365
cfg.Name = o.Name
@@ -80,7 +82,9 @@ func storeFile(ctx context.Context, s *store.Layout, fi v1.File, ro *flags.CliRo
8082
}
8183

8284
copts := getter.ClientOptions{
83-
NameOverride: fi.Name,
85+
NameOverride: fi.Name,
86+
InsecureSkipTLSVerify: derefInsecure(fi.InsecureSkipTLSVerify),
87+
CAFile: fi.CaFile,
8488
}
8589

8690
f := file.NewFile(fi.Path, file.WithClient(getter.NewClient(copts)), file.WithContext(ctx))
@@ -198,6 +202,8 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re
198202
Rewrite: o.Rewrite,
199203
ExcludeExtras: o.ExcludeExtras,
200204
Local: o.Local,
205+
CaFile: o.CaFile,
206+
InsecureSkipTLSVerify: o.InsecureSkipTLSVerify,
201207
}
202208

203209
if o.Local {
@@ -242,14 +248,16 @@ func AddImageCmd(ctx context.Context, o *flags.AddImageOpts, s *store.Layout, re
242248
func addImageVerifyConfig(o *flags.AddImageOpts) cosign.Config {
243249
switch {
244250
case o.Key != "":
245-
return cosign.Config{Key: o.Key, Tlog: o.Tlog}
251+
return cosign.Config{Key: o.Key, Tlog: o.Tlog, InsecureSkipTLSVerify: derefInsecure(o.InsecureSkipTLSVerify), CaFile: o.CaFile}
246252
case o.CertIdentityRegexp != "" || o.CertIdentity != "":
247253
return cosign.Config{
248254
CertIdentity: o.CertIdentity,
249255
CertIdentityRegexp: o.CertIdentityRegexp,
250256
CertOidcIssuer: o.CertOidcIssuer,
251257
CertOidcIssuerRegexp: o.CertOidcIssuerRegexp,
252258
CertGithubWorkflowRepository: o.CertGithubWorkflowRepository,
259+
InsecureSkipTLSVerify: derefInsecure(o.InsecureSkipTLSVerify),
260+
CaFile: o.CaFile,
253261
}
254262
default:
255263
return cosign.Config{}
@@ -320,6 +328,7 @@ func verifyAddImage(ctx context.Context, o *flags.AddImageOpts, ref string, rso
320328
}
321329

322330
func AddChartCmd(ctx context.Context, o *flags.AddChartOpts, s *store.Layout, chartName string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
331+
323332
l := log.FromContext(ctx)
324333

325334
// Nothing in the chart path forces an fsync: every descriptor it adds --
@@ -484,6 +493,9 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin
484493
}
485494
}
486495

496+
insecureSkipTLSVerify := derefInsecure(i.InsecureSkipTLSVerify)
497+
caFile := i.CaFile
498+
487499
// fetch image along with any associated signatures and attestations.
488500
// A fresh store.ImageStats is built inside the closure on every attempt,
489501
// not once outside it, so a failed attempt's partial layer/byte counts
@@ -495,7 +507,7 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin
495507
err = retry.Operation(ctx, rso, ro, func() error {
496508
attemptStats := &store.ImageStats{}
497509
var addErr error
498-
imageDigest, addErr = s.AddImage(store.WithImageStats(ctx, attemptStats), r.Name(), platform, excludeExtras, pinnedDigest)
510+
imageDigest, addErr = s.AddImage(store.WithImageStats(ctx, attemptStats), r.Name(), platform, excludeExtras, pinnedDigest, insecureSkipTLSVerify, caFile)
499511
if addErr == nil {
500512
stats = attemptStats
501513
}
@@ -564,6 +576,8 @@ func storeImage(ctx context.Context, s *store.Layout, i v1.Image, platform strin
564576
"certificate-oidc-issuer": i.CertOidcIssuer,
565577
"certificate-oidc-issuer-regexp": i.CertOidcIssuerRegexp,
566578
"certificate-github-workflow-repository": i.CertGithubWorkflowRepository,
579+
"ca-file": i.CaFile,
580+
"insecure-skip-tls-verify": i.InsecureSkipTLSVerify,
567581
"rewrite": rewrite,
568582
"exclude-extras": excludeExtras,
569583
}
@@ -807,6 +821,22 @@ func resolveChartJobs(o *flags.SyncOpts, annotations map[string]string, manifest
807821
return nil, err
808822
}
809823

824+
// caFile precedence: cli > per-chart > annotation.
825+
caFile := o.CaFile
826+
if caFile == "" {
827+
if ch.CaFile != "" {
828+
caFile = ch.CaFile
829+
} else if annotations[consts.ImageAnnotationCaFile] == "true" {
830+
caFile = annotations[consts.ImageAnnotationCaFile]
831+
}
832+
}
833+
834+
insecureSkipTLSVerify := false
835+
if o.CaFile == "" {
836+
insecureSkipTLSVerify = resolveInsecure(ch.InsecureSkipTLSVerify, annotations, o.InsecureSkipTLSVerify)
837+
} else {
838+
}
839+
810840
jobs = append(jobs, chartJob{
811841
cfg: ch,
812842
opts: flags.AddChartOpts{
@@ -820,8 +850,8 @@ func resolveChartJobs(o *flags.SyncOpts, annotations map[string]string, manifest
820850
PassCredentialsAll: ch.PassCredentialsAll,
821851
CertFile: ch.CertFile,
822852
KeyFile: ch.KeyFile,
823-
CaFile: ch.CaFile,
824-
InsecureSkipTLSVerify: ch.InsecureSkipTLSVerify,
853+
CaFile: caFile,
854+
InsecureSkipTLSVerify: insecureSkipTLSVerify,
825855
PlainHTTP: ch.PlainHTTP,
826856
},
827857
AddImages: ch.AddImages,
@@ -1350,8 +1380,17 @@ func fetchChart(ctx context.Context, s *store.Layout, j chartJob, tempRoot strin
13501380
return nil, nil, fmt.Errorf("unable to apply registry to image [%s]: %w", image, err)
13511381
}
13521382

1383+
// Chart-discovered images inherit the chart's own TLS settings --
1384+
// there is no separate per-discovered-image TLS knob in a chart
1385+
// manifest, so the registry a chart's images live in is assumed
1386+
// to share the chart repo's trust configuration.
1387+
chartInsecure := j.opts.ChartOpts.InsecureSkipTLSVerify
13531388
imageJobs = append(imageJobs, imageJob{
1354-
img: v1.Image{Name: relocated},
1389+
img: v1.Image{
1390+
Name: relocated,
1391+
CaFile: j.opts.ChartOpts.CaFile,
1392+
InsecureSkipTLSVerify: &chartInsecure,
1393+
},
13551394
platform: j.opts.Platform,
13561395
excludeExtras: j.opts.ExcludeExtras,
13571396
})

cmd/hauler/cli/store/add_test.go

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ package store
33
import (
44
"bytes"
55
"context"
6+
"crypto/ecdsa"
7+
"crypto/elliptic"
8+
"crypto/rand"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"encoding/pem"
612
"errors"
713
"fmt"
814
"io"
15+
"math/big"
916
"net"
1017
"net/http"
1118
"net/http/httptest"
@@ -252,7 +259,7 @@ func TestRewriteReference(t *testing.T) {
252259
seedImage(t, host, "src/repo", "v1", rOpts...)
253260

254261
s := newTestStore(t)
255-
if _, err := s.AddImage(ctx, host+"/src/repo:v1", "", false, "", rOpts...); err != nil {
262+
if _, err := s.AddImage(ctx, host+"/src/repo:v1", "", false, "", false, "", rOpts...); err != nil {
256263
t.Fatalf("AddImage: %v", err)
257264
}
258265

@@ -354,7 +361,7 @@ func TestRewriteReference(t *testing.T) {
354361
seedImage(t, host, "src/repo", "v1", rOpts...)
355362

356363
s := newTestStore(t)
357-
if _, err := s.AddImage(ctx, host+"/src/repo:v1", "", false, "", rOpts...); err != nil {
364+
if _, err := s.AddImage(ctx, host+"/src/repo:v1", "", false, "", false, "", rOpts...); err != nil {
358365
t.Fatalf("AddImage: %v", err)
359366
}
360367

@@ -2148,6 +2155,7 @@ func TestResolveChartJobs_NoCharts(t *testing.T) {
21482155
// TestResolveChartJobs_CredentialFields pins that every TLS/verification
21492156
// field on v1.Chart reaches the job's ChartOpts unchanged.
21502157
func TestResolveChartJobs_CredentialFields(t *testing.T) {
2158+
insecure := true
21512159
ch := v1.Chart{
21522160
Name: "rancher",
21532161
Verify: true,
@@ -2156,7 +2164,7 @@ func TestResolveChartJobs_CredentialFields(t *testing.T) {
21562164
CertFile: "/certs/client.crt",
21572165
KeyFile: "/certs/client.key",
21582166
CaFile: "/certs/ca.crt",
2159-
InsecureSkipTLSVerify: true,
2167+
InsecureSkipTLSVerify: &insecure,
21602168
PlainHTTP: true,
21612169
}
21622170

@@ -2187,8 +2195,8 @@ func TestResolveChartJobs_CredentialFields(t *testing.T) {
21872195
if opts.CaFile != ch.CaFile {
21882196
t.Errorf("CaFile = %q, want %q", opts.CaFile, ch.CaFile)
21892197
}
2190-
if opts.InsecureSkipTLSVerify != ch.InsecureSkipTLSVerify {
2191-
t.Errorf("InsecureSkipTLSVerify = %v, want %v", opts.InsecureSkipTLSVerify, ch.InsecureSkipTLSVerify)
2198+
if opts.InsecureSkipTLSVerify != derefInsecure(ch.InsecureSkipTLSVerify) {
2199+
t.Errorf("InsecureSkipTLSVerify = %v, want %v", opts.InsecureSkipTLSVerify, derefInsecure(ch.InsecureSkipTLSVerify))
21922200
}
21932201
if opts.PlainHTTP != ch.PlainHTTP {
21942202
t.Errorf("PlainHTTP = %v, want %v", opts.PlainHTTP, ch.PlainHTTP)
@@ -2972,3 +2980,100 @@ func TestFormatAddedLine_WithStats(t *testing.T) {
29722980
})
29732981
}
29742982
}
2983+
2984+
// TestStoreImage_CAFileAndInsecure exercises the insecureSkipTLSVerify / caFile
2985+
// plumbing through storeImage -> AddImage. Note: the in-memory registry runs on
2986+
// localhost, which go-containerregistry forces to http, so these cases do NOT
2987+
// perform a real TLS handshake — the actual CA trust/reject behavior is covered
2988+
// by buildTransport's handshake test in pkg/store. What's verified here is caFile
2989+
// error propagation, insecure-over-caFile precedence, and that a valid caFile
2990+
// doesn't break the pull.
2991+
func TestStoreImage_CAFileAndInsecure(t *testing.T) {
2992+
ctx := newTestContext(t)
2993+
host, rOpts := newLocalhostRegistry(t)
2994+
seedImage(t, host, "tls/repo", "v1", rOpts...)
2995+
ref := host + "/tls/repo:v1"
2996+
2997+
const missingCA = "/nonexistent/ca.pem"
2998+
2999+
t.Run("bad caFile without insecure returns error and stores nothing", func(t *testing.T) {
3000+
s := newTestStore(t)
3001+
insecure := false
3002+
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: &insecure}
3003+
err := storeImage(ctx, s, img, "", false,
3004+
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
3005+
if err == nil {
3006+
t.Fatal("expected error from unreadable caFile, got nil")
3007+
}
3008+
if n := countArtifactsInStore(t, s); n != 0 {
3009+
t.Errorf("expected nothing stored on caFile error, got %d", n)
3010+
}
3011+
})
3012+
3013+
t.Run("non-PEM caFile without insecure returns error", func(t *testing.T) {
3014+
s := newTestStore(t)
3015+
junk := filepath.Join(t.TempDir(), "junk.pem")
3016+
if err := os.WriteFile(junk, []byte("not a certificate"), 0o600); err != nil {
3017+
t.Fatal(err)
3018+
}
3019+
insecure := false
3020+
img := v1.Image{Name: ref, CaFile: junk, InsecureSkipTLSVerify: &insecure}
3021+
err := storeImage(ctx, s, img, "", false,
3022+
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
3023+
if err == nil {
3024+
t.Fatal("expected error from non-PEM caFile, got nil")
3025+
}
3026+
})
3027+
3028+
t.Run("insecure takes precedence over bad caFile", func(t *testing.T) {
3029+
s := newTestStore(t)
3030+
// insecure=true short-circuits before caFile is read; the bogus path is
3031+
// ignored and the pull still succeeds. If caFile were read first, the
3032+
// pull would error and nothing would be stored.
3033+
insecure := true
3034+
img := v1.Image{Name: ref, CaFile: missingCA, InsecureSkipTLSVerify: &insecure}
3035+
err := storeImage(ctx, s, img, "", false,
3036+
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
3037+
if err != nil {
3038+
t.Fatalf("insecure should ignore caFile, got: %v", err)
3039+
}
3040+
assertArtifactInStore(t, s, "tls/repo:v1")
3041+
})
3042+
3043+
t.Run("valid caFile without insecure is accepted", func(t *testing.T) {
3044+
s := newTestStore(t)
3045+
insecure := false
3046+
img := v1.Image{Name: ref, CaFile: writeCAFile(t), InsecureSkipTLSVerify: &insecure}
3047+
err := storeImage(ctx, s, img, "", false,
3048+
defaultRootOpts(s.Root), defaultCliOpts(), "", "", false)
3049+
if err != nil {
3050+
t.Fatalf("valid caFile should be accepted, got: %v", err)
3051+
}
3052+
assertArtifactInStore(t, s, "tls/repo:v1")
3053+
})
3054+
}
3055+
3056+
// writeCAFile writes a valid self-signed cert PEM to a temp file and returns its
3057+
// path. Its only job is to be a parseable CA file (AppendCertsFromPEM succeeds).
3058+
func writeCAFile(t *testing.T) string {
3059+
t.Helper()
3060+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
3061+
if err != nil {
3062+
t.Fatal(err)
3063+
}
3064+
tmpl := &x509.Certificate{
3065+
SerialNumber: big.NewInt(1),
3066+
Subject: pkix.Name{CommonName: "test-ca"},
3067+
NotBefore: time.Now().Add(-time.Hour),
3068+
NotAfter: time.Now().Add(time.Hour),
3069+
}
3070+
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
3071+
if err != nil {
3072+
t.Fatal(err)
3073+
}
3074+
p := filepath.Join(t.TempDir(), "ca.pem")
3075+
if err := os.WriteFile(p, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil {
3076+
t.Fatal(err)
3077+
}
3078+
return p
3079+
}

cmd/hauler/cli/store/copy_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func TestCopyCmd_Registry_SigTagDerivation(t *testing.T) {
222222

223223
// AddImage discovers and stores the .sig/.att/.sbom tags automatically.
224224
s := newTestStore(t)
225-
if _, err := s.AddImage(ctx, srcHost+"/test/signed:v1", "", false, ""); err != nil {
225+
if _, err := s.AddImage(ctx, srcHost+"/test/signed:v1", "", false, "", false, ""); err != nil {
226226
t.Fatalf("AddImage: %v", err)
227227
}
228228

0 commit comments

Comments
 (0)