diff --git a/cmd/hauler/cli/store/copy.go b/cmd/hauler/cli/store/copy.go index e99868a..c56b2bb 100644 --- a/cmd/hauler/cli/store/copy.go +++ b/cmd/hauler/cli/store/copy.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os" + "regexp" "strings" "github.com/containerd/containerd/remotes" @@ -210,6 +211,9 @@ func CopyCmd(ctx context.Context, o *flags.CopyOpts, s *store.Layout, targetRef baseRef := desc.Annotations[ocispec.AnnotationRefName] if baseRef == "" { return nil + } else if regexp.MustCompile(consts.FileExcludePattern).MatchString(baseRef) { + l.Warnf("skipping file artifact [%s]: invalid filename for registry serve", baseRef) + return nil } if o.Only != "" && !strings.Contains(baseRef, o.Only) { l.Debugf("skipping [%s] (not matching --only filter)", baseRef) diff --git a/cmd/hauler/cli/store/copy_test.go b/cmd/hauler/cli/store/copy_test.go index 28b41f5..16fbe8c 100644 --- a/cmd/hauler/cli/store/copy_test.go +++ b/cmd/hauler/cli/store/copy_test.go @@ -3,6 +3,8 @@ package store // copy_test.go covers CopyCmd for both registry:// and dir:// targets. import ( + "bytes" + "fmt" "os" "path/filepath" "strings" @@ -10,6 +12,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/rs/zerolog" "hauler.dev/go/hauler/internal/flags" v1 "hauler.dev/go/hauler/pkg/apis/hauler.cattle.io/v1" @@ -229,6 +232,65 @@ func TestCopyCmd_Registry_IgnoreErrors(t *testing.T) { } } +// TestCopyCmd_Registry_InvalidFilenameSkipTest verifies that CopyCmd emits a +// warning and skips file artifacts whose names begin with characters invalid +// as OCI tag starts, rather than attempting to push them to the registry. +func TestCopyCmd_Registry_InvalidFilenameSkipTest(t *testing.T) { + ctx := newTestContext(t) + srcDir := t.TempDir() + + files := []struct { + name string + content string + valid bool + }{ + {".test", "dot", false}, + {"-test", "dash", false}, + {"+test", "plus", false}, + {"_test", "underscore", false}, + {"valid.txt", "valid content", true}, + } + + s := newTestStore(t) + for _, pf := range files { + p := filepath.Join(srcDir, pf.name) + if err := os.WriteFile(p, []byte(pf.content), 0644); err != nil { + t.Fatalf("WriteFile %s: %v", pf.name, err) + } + if err := storeFile(ctx, s, v1.File{Path: p}); err != nil { + t.Fatalf("storeFile %s: %v", pf.name, err) + } + } + + var buf bytes.Buffer + logger := zerolog.New(&buf) + ctx = logger.WithContext(ctx) + + dstHost, _ := newTestRegistry(t) + o := &flags.CopyOpts{ + StoreRootOpts: defaultRootOpts(s.Root), + PlainHTTP: true, + } + if err := CopyCmd(ctx, o, s, "registry://"+dstHost, defaultCliOpts()); err != nil { + t.Fatalf("CopyCmd: %v", err) + } + + logs := buf.String() + + // each invalid file should have triggered a skip warning, check output to see if it contains warning string + for _, pf := range files { + if pf.valid { + continue + } + // hauler normalizes + to - in refs + refName := strings.ReplaceAll(pf.name, "+", "-") + expected := fmt.Sprintf("hauler/%s:latest", refName) + if !strings.Contains(logs, expected) || !strings.Contains(logs, "skipping file artifact") { + t.Errorf("expected skip warning for [%s] in logs, got: %s", pf.name, logs) + } + } +} + // -------------------------------------------------------------------------- // Directory copy tests // -------------------------------------------------------------------------- diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index 6fd8d6d..8a3f28a 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -1,5 +1,7 @@ package consts +import "fmt" + const ( // container media types OCIManifestSchema1 = "application/vnd.oci.image.manifest.v1+json" @@ -112,3 +114,5 @@ const ( RetriesInterval = 5 CustomTimeFormat = "2006-01-02 15:04:05" ) + +var FileExcludePattern = fmt.Sprintf(`^%s/[.\-_]`, DefaultNamespace)