From 31c93f21a3fd124e51f9938b205e07f9b1d218da Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Sun, 8 Nov 2026 22:50:01 +0000 Subject: [PATCH 1/2] install: stamp charts from the substrate module version kubectl-ate sets version.Version with ldflags, but a program that imports the installer doesn't. Its embedded charts then keep 0.1.0 and pull images that don't exist. Take the tag from the go.ate.dev/substrate dependency in the build info instead, and skip pseudo-versions. Signed-off-by: Eitan Yarmush --- pkg/kagent/install/helm.go | 35 +++++++++++++++++++++++++++--- pkg/kagent/install/install_test.go | 27 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/pkg/kagent/install/helm.go b/pkg/kagent/install/helm.go index ee1e501cd1..7e0eabd18b 100644 --- a/pkg/kagent/install/helm.go +++ b/pkg/kagent/install/helm.go @@ -23,6 +23,7 @@ import ( "log/slog" "path" "regexp" + "runtime/debug" "strings" "time" @@ -239,17 +240,45 @@ func loadChart(charts fs.FS, name string) (*chart.Chart, error) { if err != nil { return nil, fmt.Errorf("while loading chart %s: %w", path.Base(name), err) } - stampVersion(ch, version.Version) + stampVersion(ch, chartTag()) return ch, nil } -var releaseTag = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$`) +var ( + releaseTag = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$`) + // pseudoVersion matches the timestamp and commit suffix of a Go + // pseudo-version, which has no published images. + pseudoVersion = regexp.MustCompile(`\d{14}-[0-9a-f]{12}$`) +) + +// chartTag returns the release tag of this module. A program that imports +// it gets the version from its go.mod, and kubectl-ate gets it from ldflags. +func chartTag() string { + info, _ := debug.ReadBuildInfo() + return moduleTag(info, version.Version) +} + +func moduleTag(info *debug.BuildInfo, fallback string) string { + if info == nil { + return fallback + } + for _, m := range info.Deps { + if m.Path != "go.ate.dev/substrate" { + continue + } + if m.Replace != nil { + m = m.Replace + } + return m.Version + } + return fallback +} // stampVersion sets the chart and app version from a release tag, the same // way .github/scripts/release/publish-charts.sh does. Dev builds keep the // versions in Chart.yaml. func stampVersion(ch *chart.Chart, tag string) { - if !releaseTag.MatchString(tag) { + if !releaseTag.MatchString(tag) || pseudoVersion.MatchString(tag) { return } ch.Metadata.Version = strings.TrimPrefix(tag, "v") diff --git a/pkg/kagent/install/install_test.go b/pkg/kagent/install/install_test.go index 245cca59f8..d9be69d3fe 100644 --- a/pkg/kagent/install/install_test.go +++ b/pkg/kagent/install/install_test.go @@ -19,6 +19,7 @@ import ( "context" "io" "reflect" + "runtime/debug" "slices" "strings" "testing" @@ -90,6 +91,8 @@ func TestStampVersion(t *testing.T) { {"v1.2.3-rc.1", "1.2.3-rc.1", "v1.2.3-rc.1"}, {"dev", "0.1.0", "0.1.0"}, {"v1.2.3-4-gabcdef-dirty+meta", "0.1.0", "0.1.0"}, + {"v1.0.1-0.20261108120000-abcdef123456", "0.1.0", "0.1.0"}, + {"", "0.1.0", "0.1.0"}, } for _, tc := range tests { t.Run(tc.tag, func(t *testing.T) { @@ -102,6 +105,30 @@ func TestStampVersion(t *testing.T) { } } +func TestModuleTag(t *testing.T) { + dep := func(version string, replace *debug.Module) *debug.Module { + return &debug.Module{Path: "go.ate.dev/substrate", Version: version, Replace: replace} + } + tests := []struct { + name string + info *debug.BuildInfo + want string + }{ + {"no build info", nil, "fallback"}, + {"main module", &debug.BuildInfo{}, "fallback"}, + {"dependency", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.2", nil)}}, "v1.0.2"}, + {"replaced", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.0", &debug.Module{Path: "github.com/kagent-dev/substrate", Version: "v1.0.3"})}}, "v1.0.3"}, + {"local replace", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.0", &debug.Module{Path: "../substrate"})}}, ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := moduleTag(tc.info, "fallback"); got != tc.want { + t.Errorf("moduleTag() = %q, want %q", got, tc.want) + } + }) + } +} + func TestLoadEmbeddedCharts(t *testing.T) { for _, r := range allReleases { t.Run(r.chart, func(t *testing.T) { From f9d11ab3bf6bb03dfda8e52d364e50db331e08b3 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 9 Nov 2026 03:39:38 +0000 Subject: [PATCH 2/2] install: take the chart version from Options.Version Replace the build-info search with an option. The caller knows which substrate release it depends on, and the search had to guess around replaces and pseudo-versions. Signed-off-by: Eitan Yarmush --- pkg/kagent/install/helm.go | 39 +++--------------------------- pkg/kagent/install/install_test.go | 31 +++--------------------- pkg/kagent/install/installer.go | 8 ++++++ pkg/kagent/install/steps.go | 2 +- pkg/kagent/install/upgrade.go | 6 ++--- 5 files changed, 20 insertions(+), 66 deletions(-) diff --git a/pkg/kagent/install/helm.go b/pkg/kagent/install/helm.go index 7e0eabd18b..d5a4a1e5d5 100644 --- a/pkg/kagent/install/helm.go +++ b/pkg/kagent/install/helm.go @@ -23,7 +23,6 @@ import ( "log/slog" "path" "regexp" - "runtime/debug" "strings" "time" @@ -35,8 +34,6 @@ import ( releasev1 "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" "k8s.io/cli-runtime/pkg/genericclioptions" - - "go.ate.dev/substrate/internal/version" ) // release names a Helm release and the chart it installs. @@ -220,7 +217,7 @@ func asV1(rel any, err error) (*releasev1.Release, error) { } // loadChart reads the chart in directory name of charts. -func loadChart(charts fs.FS, name string) (*chart.Chart, error) { +func loadChart(charts fs.FS, name, tag string) (*chart.Chart, error) { var files []*archive.BufferedFile err := fs.WalkDir(charts, name, func(p string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { @@ -240,45 +237,17 @@ func loadChart(charts fs.FS, name string) (*chart.Chart, error) { if err != nil { return nil, fmt.Errorf("while loading chart %s: %w", path.Base(name), err) } - stampVersion(ch, chartTag()) + stampVersion(ch, tag) return ch, nil } -var ( - releaseTag = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$`) - // pseudoVersion matches the timestamp and commit suffix of a Go - // pseudo-version, which has no published images. - pseudoVersion = regexp.MustCompile(`\d{14}-[0-9a-f]{12}$`) -) - -// chartTag returns the release tag of this module. A program that imports -// it gets the version from its go.mod, and kubectl-ate gets it from ldflags. -func chartTag() string { - info, _ := debug.ReadBuildInfo() - return moduleTag(info, version.Version) -} - -func moduleTag(info *debug.BuildInfo, fallback string) string { - if info == nil { - return fallback - } - for _, m := range info.Deps { - if m.Path != "go.ate.dev/substrate" { - continue - } - if m.Replace != nil { - m = m.Replace - } - return m.Version - } - return fallback -} +var releaseTag = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$`) // stampVersion sets the chart and app version from a release tag, the same // way .github/scripts/release/publish-charts.sh does. Dev builds keep the // versions in Chart.yaml. func stampVersion(ch *chart.Chart, tag string) { - if !releaseTag.MatchString(tag) || pseudoVersion.MatchString(tag) { + if !releaseTag.MatchString(tag) { return } ch.Metadata.Version = strings.TrimPrefix(tag, "v") diff --git a/pkg/kagent/install/install_test.go b/pkg/kagent/install/install_test.go index d9be69d3fe..9cf3842494 100644 --- a/pkg/kagent/install/install_test.go +++ b/pkg/kagent/install/install_test.go @@ -19,7 +19,6 @@ import ( "context" "io" "reflect" - "runtime/debug" "slices" "strings" "testing" @@ -91,7 +90,6 @@ func TestStampVersion(t *testing.T) { {"v1.2.3-rc.1", "1.2.3-rc.1", "v1.2.3-rc.1"}, {"dev", "0.1.0", "0.1.0"}, {"v1.2.3-4-gabcdef-dirty+meta", "0.1.0", "0.1.0"}, - {"v1.0.1-0.20261108120000-abcdef123456", "0.1.0", "0.1.0"}, {"", "0.1.0", "0.1.0"}, } for _, tc := range tests { @@ -105,37 +103,16 @@ func TestStampVersion(t *testing.T) { } } -func TestModuleTag(t *testing.T) { - dep := func(version string, replace *debug.Module) *debug.Module { - return &debug.Module{Path: "go.ate.dev/substrate", Version: version, Replace: replace} - } - tests := []struct { - name string - info *debug.BuildInfo - want string - }{ - {"no build info", nil, "fallback"}, - {"main module", &debug.BuildInfo{}, "fallback"}, - {"dependency", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.2", nil)}}, "v1.0.2"}, - {"replaced", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.0", &debug.Module{Path: "github.com/kagent-dev/substrate", Version: "v1.0.3"})}}, "v1.0.3"}, - {"local replace", &debug.BuildInfo{Deps: []*debug.Module{dep("v1.0.0", &debug.Module{Path: "../substrate"})}}, ""}, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if got := moduleTag(tc.info, "fallback"); got != tc.want { - t.Errorf("moduleTag() = %q, want %q", got, tc.want) - } - }) - } -} - func TestLoadEmbeddedCharts(t *testing.T) { for _, r := range allReleases { t.Run(r.chart, func(t *testing.T) { - ch, err := loadChart(chartsfs.FS, r.chart) + ch, err := loadChart(chartsfs.FS, r.chart, "v1.2.3") if err != nil { t.Fatal(err) } + if ch.Metadata.Version != "1.2.3" || ch.Metadata.AppVersion != "v1.2.3" { + t.Errorf("got version %q app %q, want 1.2.3 v1.2.3", ch.Metadata.Version, ch.Metadata.AppVersion) + } if ch.Name() != r.chart { t.Errorf("chart name = %q, want %q", ch.Name(), r.chart) } diff --git a/pkg/kagent/install/installer.go b/pkg/kagent/install/installer.go index 45ec7df55c..2694346e69 100644 --- a/pkg/kagent/install/installer.go +++ b/pkg/kagent/install/installer.go @@ -30,6 +30,7 @@ import ( chartsfs "go.ate.dev/substrate/charts" "go.ate.dev/substrate/internal/ateclient" "go.ate.dev/substrate/internal/installdefaults" + "go.ate.dev/substrate/internal/version" ) const ( @@ -71,6 +72,10 @@ type Options struct { Charts fs.FS // HelmLog receives Helm's debug log. Nil discards it. HelmLog io.Writer + // Version is the release tag, like v1.0.1, stamped on the charts. Their + // images default to it. Empty uses version.Version, which kubectl-ate sets + // at build time. + Version string } // Installer runs install steps against one cluster. @@ -107,6 +112,9 @@ func New(opts Options, r Reporter) (*Installer, error) { if err != nil { return nil, fmt.Errorf("while creating Kubernetes client: %w", err) } + if opts.Version == "" { + opts.Version = version.Version + } charts := opts.Charts if charts == nil { charts = chartsfs.FS diff --git a/pkg/kagent/install/steps.go b/pkg/kagent/install/steps.go index 481255ef75..6041430d39 100644 --- a/pkg/kagent/install/steps.go +++ b/pkg/kagent/install/steps.go @@ -96,7 +96,7 @@ func noValues(context.Context) (map[string]any, map[string]any, error) { return // real upgrade. func (in *Installer) helmStep(r release, vf valuesFunc) func(context.Context, func(string)) (string, error) { return func(ctx context.Context, progress func(string)) (string, error) { - ch, err := loadChart(in.charts, r.chart) + ch, err := loadChart(in.charts, r.chart, in.opts.Version) if err != nil { return "", err } diff --git a/pkg/kagent/install/upgrade.go b/pkg/kagent/install/upgrade.go index e57eaf01d8..f19a9010c8 100644 --- a/pkg/kagent/install/upgrade.go +++ b/pkg/kagent/install/upgrade.go @@ -126,7 +126,7 @@ func (in *Installer) checkVersions(_ context.Context, progress func(string)) (st if st := cur.Info.Status; st.IsPending() { return "", fmt.Errorf("release %s is %s; run helm rollback or helm uninstall on it, then retry", r.name, st) } - ch, err := loadChart(in.charts, r.chart) + ch, err := loadChart(in.charts, r.chart, in.opts.Version) if err != nil { return "", err } @@ -171,7 +171,7 @@ func checkSkew(from, to string) error { // checkCRDStorage refuses to drop a CRD version that objects are still // stored in, which would make them unreadable. func (in *Installer) checkCRDStorage(ctx context.Context, _ func(string)) (string, error) { - ch, err := loadChart(in.charts, crdsRelease.chart) + ch, err := loadChart(in.charts, crdsRelease.chart, in.opts.Version) if err != nil { return "", err } @@ -234,7 +234,7 @@ func checkStoredVersions(ctx context.Context, c apiextclient.Interface, crds []a // out. func (in *Installer) diffStep(r release, vf valuesFunc, out io.Writer) func(context.Context, func(string)) (string, error) { return func(ctx context.Context, _ func(string)) (string, error) { - ch, err := loadChart(in.charts, r.chart) + ch, err := loadChart(in.charts, r.chart, in.opts.Version) if err != nil { return "", err }