11package charts
22
33import (
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
1317const 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
1629var 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.
2052func 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.
71159func AvailablePresets () ([]string , error ) {
0 commit comments