mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 20:36:30 +00:00
warning message if hauler store created before 2.1.0 provenance
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
gname "github.com/google/go-containerregistry/pkg/name"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/mod/semver"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"hauler.dev/go/hauler/v2/internal/flags"
|
||||
@@ -65,6 +66,13 @@ func CreateManifestCmd(ctx context.Context, o *flags.CreateManifestOpts, s *stor
|
||||
l.SetLevel("fatal")
|
||||
}
|
||||
|
||||
// Warn when the store predates the provenance metadata this command relies on
|
||||
// to faithfully reconstruct the manifest. Written to stderr so it stays visible
|
||||
// even in stdout mode (where the logger is silenced and stdout carries the YAML).
|
||||
if version, err := readStoreHaulerVersion(s.Root); err != nil || storeLacksProvenance(version) {
|
||||
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.")
|
||||
}
|
||||
|
||||
var images []manifestImage
|
||||
var charts []manifestChart
|
||||
var files []manifestFile
|
||||
@@ -237,6 +245,50 @@ func CreateManifestCmd(ctx context.Context, o *flags.CreateManifestOpts, s *stor
|
||||
return nil
|
||||
}
|
||||
|
||||
// provenanceMinVersion is the first Hauler release whose stores record enough
|
||||
// provenance metadata for `store create manifest` to faithfully reconstruct
|
||||
// them. Stores written by earlier versions (or with no recorded version) get a
|
||||
// best-effort manifest and a warning.
|
||||
const provenanceMinVersion = "v2.1.0"
|
||||
|
||||
// storeVersionMetadata mirrors the subset of store.json this command reads to
|
||||
// decide whether the store carries reliable provenance metadata.
|
||||
type storeVersionMetadata struct {
|
||||
HaulerVersion string `json:"hauler-version"`
|
||||
}
|
||||
|
||||
// readStoreHaulerVersion returns the "hauler-version" recorded in the store's
|
||||
// store.json, or an error if the file is missing or unparseable.
|
||||
func readStoreHaulerVersion(root string) (string, error) {
|
||||
data, err := os.ReadFile(filepath.Join(root, consts.DefaultStoreMetadataName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var m storeVersionMetadata
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return m.HaulerVersion, nil
|
||||
}
|
||||
|
||||
// storeLacksProvenance reports whether a store written by haulerVersion predates
|
||||
// provenanceMinVersion. An empty or unparseable version is treated as lacking
|
||||
// provenance. The comparison is by major.minor so that pre-releases of the
|
||||
// threshold (e.g. v2.1.0-rc1) are not flagged.
|
||||
func storeLacksProvenance(haulerVersion string) bool {
|
||||
v := strings.TrimSpace(haulerVersion)
|
||||
if v == "" {
|
||||
return true
|
||||
}
|
||||
if !strings.HasPrefix(v, "v") {
|
||||
v = "v" + v
|
||||
}
|
||||
if !semver.IsValid(v) {
|
||||
return true
|
||||
}
|
||||
return semver.Compare(semver.MajorMinor(v), semver.MajorMinor(provenanceMinVersion)) < 0
|
||||
}
|
||||
|
||||
func writeDoc(out *strings.Builder, header string, kind string, name string, spec interface{}) error {
|
||||
doc := manifestDoc{
|
||||
APIVersion: consts.ContentGroup + "/v1",
|
||||
|
||||
@@ -327,6 +327,67 @@ func TestCreateManifestCmd_MixedContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLacksProvenance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
{name: "empty version", version: "", want: true},
|
||||
{name: "whitespace only", version: " ", want: true},
|
||||
{name: "unparseable", version: "not-a-version", want: true},
|
||||
{name: "older patch", version: "v2.0.2", want: true},
|
||||
{name: "older minor", version: "v2.0.99", want: true},
|
||||
{name: "older major", version: "v1.9.9", want: true},
|
||||
{name: "pseudo-version before threshold", version: "v2.0.2-0.20260728211252-c6fbcc97b769+dirty", want: true},
|
||||
{name: "threshold exactly", version: "v2.1.0", want: false},
|
||||
{name: "threshold pre-release", version: "v2.1.0-rc1", want: false},
|
||||
{name: "newer patch", version: "v2.1.5", want: false},
|
||||
{name: "newer major", version: "v3.0.0", want: false},
|
||||
{name: "missing v prefix still parses", version: "2.0.2", want: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := storeLacksProvenance(tc.version); got != tc.want {
|
||||
t.Errorf("storeLacksProvenance(%q) = %v, want %v", tc.version, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadStoreHaulerVersion(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "store.json")
|
||||
if err := os.WriteFile(path, []byte(`{"store-id":"abc","hauler-version":"v2.0.2"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := readStoreHaulerVersion(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("readStoreHaulerVersion: %v", err)
|
||||
}
|
||||
if got != "v2.0.2" {
|
||||
t.Errorf("readStoreHaulerVersion = %q, want %q", got, "v2.0.2")
|
||||
}
|
||||
|
||||
// A store.json with no hauler-version field yields an empty string (which the
|
||||
// caller treats as lacking provenance).
|
||||
if err := os.WriteFile(path, []byte(`{"store-id":"abc"}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err = readStoreHaulerVersion(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("readStoreHaulerVersion (no version): %v", err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Errorf("readStoreHaulerVersion (no version) = %q, want empty", got)
|
||||
}
|
||||
|
||||
// A missing store.json is surfaced as an error.
|
||||
if _, err := readStoreHaulerVersion(t.TempDir()); err == nil {
|
||||
t.Error("expected error reading version from a directory with no store.json")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeOriginalChartRef(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user