Skip to content

Commit 3a03628

Browse files
committed
warning message if hauler store created before 2.1.0 provenance
1 parent bbca062 commit 3a03628

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

cmd/hauler/cli/store/create_manifest.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
gname "github.com/google/go-containerregistry/pkg/name"
1212
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
13+
"golang.org/x/mod/semver"
1314
"gopkg.in/yaml.v3"
1415

1516
"hauler.dev/go/hauler/v2/internal/flags"
@@ -65,6 +66,13 @@ func CreateManifestCmd(ctx context.Context, o *flags.CreateManifestOpts, s *stor
6566
l.SetLevel("fatal")
6667
}
6768

69+
// Warn when the store predates the provenance metadata this command relies on
70+
// to faithfully reconstruct the manifest. Written to stderr so it stays visible
71+
// even in stdout mode (where the logger is silenced and stdout carries the YAML).
72+
if version, err := readStoreHaulerVersion(s.Root); err != nil || storeLacksProvenance(version) {
73+
fmt.Fprintln(os.Stderr, "WARNING: The version of Hauler used to create this store did not include provenance metadata to reconstruct the manifest. Please confirm the generated manifest is accurate.")
74+
}
75+
6876
var images []manifestImage
6977
var charts []manifestChart
7078
var files []manifestFile
@@ -237,6 +245,50 @@ func CreateManifestCmd(ctx context.Context, o *flags.CreateManifestOpts, s *stor
237245
return nil
238246
}
239247

248+
// provenanceMinVersion is the first Hauler release whose stores record enough
249+
// provenance metadata for `store create manifest` to faithfully reconstruct
250+
// them. Stores written by earlier versions (or with no recorded version) get a
251+
// best-effort manifest and a warning.
252+
const provenanceMinVersion = "v2.1.0"
253+
254+
// storeVersionMetadata mirrors the subset of store.json this command reads to
255+
// decide whether the store carries reliable provenance metadata.
256+
type storeVersionMetadata struct {
257+
HaulerVersion string `json:"hauler-version"`
258+
}
259+
260+
// readStoreHaulerVersion returns the "hauler-version" recorded in the store's
261+
// store.json, or an error if the file is missing or unparseable.
262+
func readStoreHaulerVersion(root string) (string, error) {
263+
data, err := os.ReadFile(filepath.Join(root, consts.DefaultStoreMetadataName))
264+
if err != nil {
265+
return "", err
266+
}
267+
var m storeVersionMetadata
268+
if err := json.Unmarshal(data, &m); err != nil {
269+
return "", err
270+
}
271+
return m.HaulerVersion, nil
272+
}
273+
274+
// storeLacksProvenance reports whether a store written by haulerVersion predates
275+
// provenanceMinVersion. An empty or unparseable version is treated as lacking
276+
// provenance. The comparison is by major.minor so that pre-releases of the
277+
// threshold (e.g. v2.1.0-rc1) are not flagged.
278+
func storeLacksProvenance(haulerVersion string) bool {
279+
v := strings.TrimSpace(haulerVersion)
280+
if v == "" {
281+
return true
282+
}
283+
if !strings.HasPrefix(v, "v") {
284+
v = "v" + v
285+
}
286+
if !semver.IsValid(v) {
287+
return true
288+
}
289+
return semver.Compare(semver.MajorMinor(v), semver.MajorMinor(provenanceMinVersion)) < 0
290+
}
291+
240292
func writeDoc(out *strings.Builder, header string, kind string, name string, spec interface{}) error {
241293
doc := manifestDoc{
242294
APIVersion: consts.ContentGroup + "/v1",

cmd/hauler/cli/store/create_manifest_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,67 @@ func TestCreateManifestCmd_MixedContent(t *testing.T) {
327327
}
328328
}
329329

330+
func TestStoreLacksProvenance(t *testing.T) {
331+
tests := []struct {
332+
name string
333+
version string
334+
want bool
335+
}{
336+
{name: "empty version", version: "", want: true},
337+
{name: "whitespace only", version: " ", want: true},
338+
{name: "unparseable", version: "not-a-version", want: true},
339+
{name: "older patch", version: "v2.0.2", want: true},
340+
{name: "older minor", version: "v2.0.99", want: true},
341+
{name: "older major", version: "v1.9.9", want: true},
342+
{name: "pseudo-version before threshold", version: "v2.0.2-0.20260728211252-c6fbcc97b769+dirty", want: true},
343+
{name: "threshold exactly", version: "v2.1.0", want: false},
344+
{name: "threshold pre-release", version: "v2.1.0-rc1", want: false},
345+
{name: "newer patch", version: "v2.1.5", want: false},
346+
{name: "newer major", version: "v3.0.0", want: false},
347+
{name: "missing v prefix still parses", version: "2.0.2", want: true},
348+
}
349+
for _, tc := range tests {
350+
t.Run(tc.name, func(t *testing.T) {
351+
if got := storeLacksProvenance(tc.version); got != tc.want {
352+
t.Errorf("storeLacksProvenance(%q) = %v, want %v", tc.version, got, tc.want)
353+
}
354+
})
355+
}
356+
}
357+
358+
func TestReadStoreHaulerVersion(t *testing.T) {
359+
dir := t.TempDir()
360+
path := filepath.Join(dir, "store.json")
361+
if err := os.WriteFile(path, []byte(`{"store-id":"abc","hauler-version":"v2.0.2"}`), 0o644); err != nil {
362+
t.Fatal(err)
363+
}
364+
got, err := readStoreHaulerVersion(dir)
365+
if err != nil {
366+
t.Fatalf("readStoreHaulerVersion: %v", err)
367+
}
368+
if got != "v2.0.2" {
369+
t.Errorf("readStoreHaulerVersion = %q, want %q", got, "v2.0.2")
370+
}
371+
372+
// A store.json with no hauler-version field yields an empty string (which the
373+
// caller treats as lacking provenance).
374+
if err := os.WriteFile(path, []byte(`{"store-id":"abc"}`), 0o644); err != nil {
375+
t.Fatal(err)
376+
}
377+
got, err = readStoreHaulerVersion(dir)
378+
if err != nil {
379+
t.Fatalf("readStoreHaulerVersion (no version): %v", err)
380+
}
381+
if got != "" {
382+
t.Errorf("readStoreHaulerVersion (no version) = %q, want empty", got)
383+
}
384+
385+
// A missing store.json is surfaced as an error.
386+
if _, err := readStoreHaulerVersion(t.TempDir()); err == nil {
387+
t.Error("expected error reading version from a directory with no store.json")
388+
}
389+
}
390+
330391
func TestDecodeOriginalChartRef(t *testing.T) {
331392
tests := []struct {
332393
name string

0 commit comments

Comments
 (0)