Skip to content

Commit 8c9288b

Browse files
committed
feat(charts): add content digest for the embedded talm library
Expose the binary's built-in talm library chart as a normalized path->content map (TalmLibraryFiles) plus a deterministic, order- independent digest over a chart tree (HashChartFiles). Chart.yaml name/version lines are folded to %s placeholders through a shared NormalizeChartMeta helper — also adopted by PresetFiles — so a version stamp is not counted as content. This is the comparison primitive for detecting when a project's vendored charts/talm/ has drifted from the binary that renders it: equal digests mean identical library bytes, independent of the stamped version. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
1 parent cb340ab commit 8c9288b

3 files changed

Lines changed: 253 additions & 7 deletions

File tree

charts/charts.go

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,56 @@
11
package charts
22

33
import (
4+
"crypto/sha256"
45
"embed"
6+
"encoding/hex"
7+
"fmt"
58
"io/fs"
69
"path"
710
"regexp"
11+
"sort"
812
"strings"
913

1014
"github.com/cockroachdb/errors"
1115
)
1216

1317
const presetGenericName = "generic"
1418

19+
// talmLibraryName is the directory of the talm library chart inside the
20+
// embedded tree (and the name it is vendored under at charts/talm/ in a
21+
// project). It is talm-owned — unlike the preset templates, which the
22+
// operator edits — so it is the only tree the drift check compares.
23+
const talmLibraryName = "talm"
24+
25+
// chartYamlName is the conventional Helm chart metadata filename.
26+
const chartYamlName = "Chart.yaml"
27+
1528
//go:embed all:cozystack all:generic all:talm
1629
var embeddedCharts embed.FS
1730

31+
// chartMetaRegex matches the `name:`/`version:` metadata lines of a
32+
// Chart.yaml. Both are rewritten to a %s placeholder so the init flow can
33+
// substitute the real project name/version, and so the drift check can
34+
// compare two chart trees without a version stamp counting as content.
35+
var chartMetaRegex = regexp.MustCompile(`(name|version): \S+`)
36+
37+
// NormalizeChartMeta rewrites the name/version lines of a Chart.yaml to %s
38+
// placeholders. Files other than Chart.yaml pass through unchanged. base is
39+
// the file's base name (e.g. path.Base(p)). Keeping a single normalizer
40+
// means the init-time substitution and the content-drift comparison treat
41+
// chart metadata identically.
42+
func NormalizeChartMeta(base, content string) string {
43+
if base != chartYamlName {
44+
return content
45+
}
46+
47+
return chartMetaRegex.ReplaceAllString(content, "$1: %s")
48+
}
49+
1850
// PresetFiles returns a map of file paths to their contents.
1951
// For Chart.yaml files, name and version are replaced with %s placeholders.
2052
func PresetFiles() (map[string]string, error) {
2153
filesMap := make(map[string]string)
22-
regex := regexp.MustCompile(`(name|version): \S+`)
2354

2455
err := fs.WalkDir(embeddedCharts, ".", func(filePath string, entry fs.DirEntry, err error) error {
2556
if err != nil {
@@ -47,12 +78,8 @@ func PresetFiles() (map[string]string, error) {
4778
return errors.Wrapf(err, "reading embedded chart file %q", filePath)
4879
}
4980

50-
content := string(data)
51-
52-
// For Chart.yaml files, replace name and version with %s
53-
if path.Base(filePath) == "Chart.yaml" {
54-
content = regex.ReplaceAllString(content, "$1: %s")
55-
}
81+
// For Chart.yaml files, replace name and version with %s.
82+
content := NormalizeChartMeta(path.Base(filePath), string(data))
5683

5784
// Use the file path as-is (relative to charts directory)
5885
filesMap[filePath] = content
@@ -66,6 +93,67 @@ func PresetFiles() (map[string]string, error) {
6693
return filesMap, nil
6794
}
6895

96+
// TalmLibraryFiles returns the embedded talm library chart keyed by path
97+
// relative to the talm/ root (e.g. "Chart.yaml", "templates/_helpers.tpl"),
98+
// so the keys line up with a project's vendored charts/talm/ tree. Chart.yaml
99+
// metadata is normalized via NormalizeChartMeta, so a version stamp is not
100+
// treated as content. It is the embedded counterpart compared against the
101+
// vendored copy to surface chart drift after a binary upgrade.
102+
func TalmLibraryFiles() (map[string]string, error) {
103+
filesMap := make(map[string]string)
104+
105+
err := fs.WalkDir(embeddedCharts, talmLibraryName, func(filePath string, entry fs.DirEntry, err error) error {
106+
if err != nil {
107+
return errors.Wrapf(err, "walking embedded talm library at %q", filePath)
108+
}
109+
110+
if entry.IsDir() {
111+
return nil
112+
}
113+
114+
data, err := embeddedCharts.ReadFile(filePath)
115+
if err != nil {
116+
return errors.Wrapf(err, "reading embedded talm file %q", filePath)
117+
}
118+
119+
// Strip the talm/ prefix so keys match a vendored charts/talm/ tree.
120+
rel := strings.TrimPrefix(filePath, talmLibraryName+"/")
121+
filesMap[rel] = NormalizeChartMeta(path.Base(filePath), string(data))
122+
123+
return nil
124+
})
125+
if err != nil {
126+
return nil, errors.Wrap(err, "collecting embedded talm library")
127+
}
128+
129+
return filesMap, nil
130+
}
131+
132+
// HashChartFiles returns a deterministic digest of a chart tree described as
133+
// a path→content map. The digest folds in the sorted set of (path, content)
134+
// pairs and is independent of map iteration order. Each path and content is
135+
// length-prefixed so distinct trees cannot collide by a fortunate alignment
136+
// of concatenated bytes. Two trees hash equal iff they carry the same files
137+
// with the same bytes — the signal the drift check relies on.
138+
func HashChartFiles(files map[string]string) string {
139+
paths := make([]string, 0, len(files))
140+
for p := range files {
141+
paths = append(paths, p)
142+
}
143+
144+
sort.Strings(paths)
145+
146+
hasher := sha256.New()
147+
for _, p := range paths {
148+
// Length-prefix both the path and its content so the boundary
149+
// between them (and between successive entries) is unambiguous.
150+
fmt.Fprintf(hasher, "%d:%s%d:", len(p), p, len(files[p]))
151+
hasher.Write([]byte(files[p]))
152+
}
153+
154+
return hex.EncodeToString(hasher.Sum(nil))
155+
}
156+
69157
// AvailablePresets returns a list of available preset chart names.
70158
// The presetGenericName preset is always first if it exists.
71159
func AvailablePresets() ([]string, error) {

charts/library_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package charts
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
// TestTalmLibraryFiles_NormalizesAndStripsPrefix pins the embedded talm
9+
// library collector. Keys must be relative to the talm/ root (so they
10+
// line up with a project's vendored charts/talm/ tree), the library
11+
// Chart.yaml must come back with its name/version normalized to %s (so a
12+
// version stamp never counts as content), and the helpers template must
13+
// be present verbatim.
14+
func TestTalmLibraryFiles_NormalizesAndStripsPrefix(t *testing.T) {
15+
files, err := TalmLibraryFiles()
16+
if err != nil {
17+
t.Fatalf("TalmLibraryFiles: %v", err)
18+
}
19+
20+
if len(files) == 0 {
21+
t.Fatal("TalmLibraryFiles returned empty map; embedded talm library is missing")
22+
}
23+
24+
for path := range files {
25+
if strings.HasPrefix(path, "talm/") {
26+
t.Errorf("key %q still carries the talm/ prefix; it would not match a vendored charts/talm/ relative path", path)
27+
}
28+
}
29+
30+
chart, ok := files["Chart.yaml"]
31+
if !ok {
32+
t.Fatal("expected Chart.yaml entry keyed relative to the talm/ root")
33+
}
34+
if !strings.Contains(chart, "version: %s") {
35+
t.Errorf("library Chart.yaml not normalized; a version stamp would be treated as content drift:\n%s", chart)
36+
}
37+
38+
if _, ok := files["templates/_helpers.tpl"]; !ok {
39+
t.Error("expected templates/_helpers.tpl in the embedded talm library")
40+
}
41+
}
42+
43+
// TestHashChartFiles_OrderIndependent pins that the digest depends on the
44+
// set of (path, content) pairs, not on map iteration order. Go map order
45+
// is randomized, so a digest that folded order in would be non-
46+
// deterministic across runs and falsely report drift.
47+
func TestHashChartFiles_OrderIndependent(t *testing.T) {
48+
a := map[string]string{
49+
"Chart.yaml": "name: %s\nversion: %s\n",
50+
"templates/_helpers.tpl": "{{- define \"x\" -}}{{- end -}}",
51+
}
52+
b := map[string]string{
53+
"templates/_helpers.tpl": "{{- define \"x\" -}}{{- end -}}",
54+
"Chart.yaml": "name: %s\nversion: %s\n",
55+
}
56+
57+
if HashChartFiles(a) != HashChartFiles(b) {
58+
t.Error("digest depends on map order; it must be deterministic over the (path, content) set")
59+
}
60+
}
61+
62+
// TestHashChartFiles_ContentSensitive pins that any change to a file's
63+
// content changes the digest. This is the signal the drift check relies
64+
// on: a stale helpers template must hash differently from a fresh one.
65+
func TestHashChartFiles_ContentSensitive(t *testing.T) {
66+
base := map[string]string{"templates/_helpers.tpl": "old"}
67+
changed := map[string]string{"templates/_helpers.tpl": "new"}
68+
69+
if HashChartFiles(base) == HashChartFiles(changed) {
70+
t.Error("digest is insensitive to content; real chart drift would go undetected")
71+
}
72+
}
73+
74+
// TestHashChartFiles_PathBoundaryUnambiguous guards the length-prefixed
75+
// framing: two different file trees must not collide just because their
76+
// concatenated path+content bytes happen to line up. Without framing,
77+
// {"ab": "c"} and {"a": "bc"} would hash identically.
78+
func TestHashChartFiles_PathBoundaryUnambiguous(t *testing.T) {
79+
left := map[string]string{"ab": "c"}
80+
right := map[string]string{"a": "bc"}
81+
82+
if HashChartFiles(left) == HashChartFiles(right) {
83+
t.Error("path/content boundary is ambiguous; distinct trees collide to the same digest")
84+
}
85+
}
86+
87+
// TestNormalizeChartMeta_VersionStampDoesNotAffectHash is the core
88+
// correctness guard for the whole drift feature: two library trees that
89+
// differ ONLY in the Chart.yaml version stamp must hash identically once
90+
// normalized. A binary version bump that left charts/talm/ byte-identical
91+
// must not be reported as drift — that false positive is exactly what the
92+
// version-number comparison approach got wrong.
93+
func TestNormalizeChartMeta_VersionStampDoesNotAffectHash(t *testing.T) {
94+
old := map[string]string{
95+
"Chart.yaml": NormalizeChartMeta("Chart.yaml", "name: talm\nversion: 0.27.0\ntype: library\n"),
96+
"templates/_helpers.tpl": "{{- define \"x\" -}}{{- end -}}",
97+
}
98+
fresh := map[string]string{
99+
"Chart.yaml": NormalizeChartMeta("Chart.yaml", "name: talm\nversion: 0.30.0\ntype: library\n"),
100+
"templates/_helpers.tpl": "{{- define \"x\" -}}{{- end -}}",
101+
}
102+
103+
if HashChartFiles(old) != HashChartFiles(fresh) {
104+
t.Error("version-only difference changed the digest; a pure version bump would raise a false drift warning")
105+
}
106+
}
107+
108+
// TestNormalizeChartMeta_LeavesNonChartYamlUntouched pins that the
109+
// normalizer only rewrites Chart.yaml. A helpers template that happens to
110+
// contain a `version:` line (e.g. inside a rendered manifest snippet) must
111+
// pass through unchanged, or genuine drift in that template would be
112+
// masked.
113+
func TestNormalizeChartMeta_LeavesNonChartYamlUntouched(t *testing.T) {
114+
const tpl = "version: 1.2.3 # part of a rendered example, not chart metadata"
115+
if got := NormalizeChartMeta("_helpers.tpl", tpl); got != tpl {
116+
t.Errorf("NormalizeChartMeta rewrote a non-Chart.yaml file: %q", got)
117+
}
118+
}
119+
120+
// TestNormalizeChartMeta_PreservesApiAndAppVersion pins that only the
121+
// `name`/`version` metadata is folded to a placeholder. The camelCase
122+
// `apiVersion`/`appVersion` keys must survive verbatim — otherwise a real
123+
// change to those fields would be normalized away and hidden from the drift
124+
// comparison. Guards against a future regex tweak (e.g. adding `(?i)`) that
125+
// would start eating them.
126+
func TestNormalizeChartMeta_PreservesApiAndAppVersion(t *testing.T) {
127+
const chart = "apiVersion: v2\nname: talm\nversion: 0.1.0\nappVersion: 1.30.0\ntype: library\n"
128+
129+
got := NormalizeChartMeta("Chart.yaml", chart)
130+
131+
if !strings.Contains(got, "apiVersion: v2") {
132+
t.Errorf("apiVersion was rewritten:\n%s", got)
133+
}
134+
if !strings.Contains(got, "appVersion: 1.30.0") {
135+
t.Errorf("appVersion was rewritten:\n%s", got)
136+
}
137+
if !strings.Contains(got, "name: %s") || !strings.Contains(got, "version: %s") {
138+
t.Errorf("name/version not normalized:\n%s", got)
139+
}
140+
}

pkg/generated/presets.go

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)