Skip to content

Commit a894154

Browse files
committed
add Chart image baking
Signed-off-by: Tim Ramlot <[email protected]>
1 parent 9a9f933 commit a894154

File tree

5 files changed

+473
-4
lines changed

5 files changed

+473
-4
lines changed

baker/bake.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package baker
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"slices"
8+
"strings"
9+
10+
"github.com/google/go-containerregistry/pkg/name"
11+
"github.com/google/go-containerregistry/pkg/v1/remote"
12+
)
13+
14+
type BakeReference struct {
15+
Repository string
16+
Tag string
17+
Digest string
18+
}
19+
20+
func ParseBakeReference(value string) (bakeInput BakeReference) {
21+
// extract digest from value
22+
if digestRef, err := name.NewDigest(value); err == nil {
23+
bakeInput.Repository = digestRef.Context().String()
24+
bakeInput.Digest = digestRef.DigestStr()
25+
}
26+
27+
// extract tag from value
28+
if tagRef, err := name.NewTag(value); err == nil {
29+
bakeInput.Repository = tagRef.Context().String()
30+
bakeInput.Tag = tagRef.TagStr()
31+
}
32+
33+
return bakeInput
34+
}
35+
36+
func (br BakeReference) Reference() name.Reference {
37+
repo, _ := name.NewRepository(br.Repository)
38+
if br.Digest != "" {
39+
return repo.Digest(br.Digest)
40+
}
41+
return repo.Tag(br.Tag)
42+
}
43+
44+
func (br BakeReference) String() string {
45+
var builder strings.Builder
46+
_, _ = builder.WriteString(br.Repository)
47+
if br.Tag != "" {
48+
_, _ = builder.WriteString(":")
49+
_, _ = builder.WriteString(br.Tag)
50+
}
51+
if br.Digest != "" {
52+
_, _ = builder.WriteString("@")
53+
_, _ = builder.WriteString(br.Digest)
54+
}
55+
return builder.String()
56+
}
57+
58+
type BakeInput = BakeReference
59+
60+
func (bi BakeInput) Find(ctx context.Context) (BakeOutput, error) {
61+
desc, err := remote.Head(bi.Reference(), remote.WithContext(ctx))
62+
if err != nil {
63+
return BakeReference{}, fmt.Errorf("failed to pull %s", bi)
64+
}
65+
66+
return BakeReference{
67+
Repository: bi.Repository,
68+
Digest: desc.Digest.String(),
69+
Tag: bi.Tag,
70+
}, nil
71+
}
72+
73+
type BakeOutput = BakeReference
74+
75+
func Extract(ctx context.Context, inputPath string) (map[string]BakeInput, error) {
76+
results := map[string]BakeInput{}
77+
78+
values, err := readValuesYAML(inputPath)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
if _, err := allNestedStringValues(values, nil, func(path []string, value string) (string, error) {
84+
if path[len(path)-1] != "_defaultReference" {
85+
return value, nil
86+
}
87+
88+
bakeInput := ParseBakeReference(value)
89+
if bakeInput == (BakeInput{}) {
90+
return "", fmt.Errorf("invalid _defaultReference value: %q", value)
91+
}
92+
93+
results[strings.Join(path, ".")] = bakeInput
94+
95+
return value, nil
96+
}); err != nil {
97+
return nil, err
98+
}
99+
100+
return results, nil
101+
}
102+
103+
type BakeAction struct {
104+
In BakeInput `json:"in"`
105+
Out BakeOutput `json:"out"`
106+
}
107+
108+
func Bake(ctx context.Context, inputPath string, valuesPaths []string) (map[string]BakeAction, error) {
109+
results := map[string]BakeAction{}
110+
return results, inplaceModifyValuesYAML(inputPath, func(values map[string]any) (map[string]any, error) {
111+
replacedValuePaths := map[string]struct{}{}
112+
newValues, err := allNestedStringValues(values, nil, func(path []string, value string) (string, error) {
113+
if path[len(path)-1] != "_defaultReference" {
114+
return value, nil
115+
}
116+
117+
bakeInput := ParseBakeReference(value)
118+
if bakeInput == (BakeInput{}) {
119+
return "", fmt.Errorf("invalid _defaultReference value: %q", value)
120+
}
121+
122+
bakeOutput, err := bakeInput.Find(ctx)
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
pathString := strings.Join(path, ".")
128+
replacedValuePaths[pathString] = struct{}{}
129+
results[pathString] = BakeAction{
130+
In: bakeInput,
131+
Out: bakeOutput,
132+
}
133+
134+
return bakeOutput.String(), nil
135+
})
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
if len(replacedValuePaths) > len(valuesPaths) {
141+
return nil, fmt.Errorf("too many value paths were replaced: %v", slices.Collect(maps.Keys(replacedValuePaths)))
142+
}
143+
for _, valuesPath := range valuesPaths {
144+
if _, ok := replacedValuePaths[valuesPath]; !ok {
145+
return nil, fmt.Errorf("path was not replaced: %s", valuesPath)
146+
}
147+
}
148+
149+
return newValues.(map[string]any), nil
150+
})
151+
}
152+
153+
func allNestedStringValues(object any, path []string, fn func(path []string, value string) (string, error)) (any, error) {
154+
switch t := object.(type) {
155+
case map[string]any:
156+
for key, value := range t {
157+
keyPath := append(path, key)
158+
if stringValue, ok := value.(string); ok {
159+
newValue, err := fn(slices.Clone(keyPath), stringValue)
160+
if err != nil {
161+
return nil, err
162+
}
163+
t[key] = newValue
164+
} else {
165+
newValue, err := allNestedStringValues(value, keyPath, fn)
166+
if err != nil {
167+
return nil, err
168+
}
169+
t[key] = newValue
170+
}
171+
}
172+
case map[string]string:
173+
for key, stringValue := range t {
174+
keyPath := append(path, key)
175+
newValue, err := fn(slices.Clone(keyPath), stringValue)
176+
if err != nil {
177+
return nil, err
178+
}
179+
t[key] = newValue
180+
}
181+
case []any:
182+
for i, value := range t {
183+
path = append(path, fmt.Sprintf("%d", i))
184+
newValue, err := allNestedStringValues(value, path, fn)
185+
if err != nil {
186+
return nil, err
187+
}
188+
t[i] = newValue
189+
}
190+
default:
191+
// ignore object
192+
}
193+
194+
return object, nil
195+
}

baker/modify_values.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package baker
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"compress/gzip"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"os"
11+
"strings"
12+
13+
"gopkg.in/yaml.v3"
14+
)
15+
16+
// inplaceModifyValuesYAML modifies the provided chart tar file and updates the values.yaml file.
17+
func inplaceModifyValuesYAML(inputPath string, modFn func(map[string]any) (map[string]any, error)) error {
18+
tempOut := fmt.Sprintf("%s.tmp", inputPath)
19+
20+
if err := modifyValuesYAML(inputPath, tempOut, modFn); err != nil {
21+
return errors.Join(err, os.Remove(tempOut))
22+
} else {
23+
return os.Rename(tempOut, inputPath)
24+
}
25+
}
26+
27+
// inplaceReadValuesYAML reads the provided chart tar file and returns the values
28+
func readValuesYAML(inputPath string) (map[string]any, error) {
29+
var result map[string]any
30+
return result, modifyValuesYAML(inputPath, "", func(m map[string]any) (map[string]any, error) {
31+
result = m
32+
return m, nil
33+
})
34+
}
35+
36+
type modFunction func(map[string]any) (map[string]any, error)
37+
38+
func modifyValuesYAML(inFilePath string, outFilePath string, modFn modFunction) error {
39+
inReader, err := os.Open(inFilePath)
40+
if err != nil {
41+
return err
42+
}
43+
defer inReader.Close()
44+
45+
outWriter := io.Discard
46+
if outFilePath != "" {
47+
outFile, err := os.Create(outFilePath)
48+
if err != nil {
49+
return err
50+
}
51+
defer outFile.Close()
52+
53+
outWriter = outFile
54+
}
55+
56+
if strings.HasSuffix(inFilePath, ".tgz") {
57+
if err := modifyTarStreamValuesYAML(inReader, outWriter, modFn); err != nil {
58+
return err
59+
}
60+
} else {
61+
if err := modifyStreamValuesYAML(inReader, outWriter, modFn); err != nil {
62+
return err
63+
}
64+
}
65+
66+
return nil
67+
}
68+
69+
func modifyTarStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
70+
inFileDecompressed, err := gzip.NewReader(in)
71+
if err != nil {
72+
return err
73+
}
74+
defer inFileDecompressed.Close()
75+
tr := tar.NewReader(inFileDecompressed)
76+
77+
outFileCompressed, err := gzip.NewWriterLevel(out, gzip.BestCompression)
78+
if err != nil {
79+
return err
80+
}
81+
// see https://github.com/helm/helm/blob/b25a51b291b930ef05ed0f4a9ea7cce073c10f63/pkg/chart/v2/util/save.go#L35C28-L35C67
82+
// seems like an easter egg, but keep it for "backwards compatibility" :)
83+
outFileCompressed.Extra = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
84+
outFileCompressed.Comment = "Helm"
85+
defer outFileCompressed.Close()
86+
tw := tar.NewWriter(outFileCompressed)
87+
defer tw.Close()
88+
89+
for {
90+
hdr, err := tr.Next()
91+
if err == io.EOF {
92+
break // End of archive
93+
}
94+
if err != nil {
95+
return err
96+
}
97+
98+
if strings.HasSuffix(hdr.Name, "/values.yaml") {
99+
var modifiedContent bytes.Buffer
100+
if err := modifyStreamValuesYAML(tr, &modifiedContent, modFn); err != nil {
101+
return err
102+
}
103+
104+
// Update header size
105+
hdr.Size = int64(modifiedContent.Len())
106+
107+
// Write updated header and content
108+
if err := tw.WriteHeader(hdr); err != nil {
109+
return err
110+
}
111+
if _, err := tw.Write(modifiedContent.Bytes()); err != nil {
112+
return err
113+
}
114+
} else {
115+
// Stream other files unchanged
116+
if err := tw.WriteHeader(hdr); err != nil {
117+
return err
118+
}
119+
if _, err := io.Copy(tw, tr); err != nil {
120+
return err
121+
}
122+
}
123+
}
124+
125+
return nil
126+
}
127+
128+
func modifyStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
129+
// Parse YAML
130+
var data map[string]any
131+
if err := yaml.NewDecoder(in).Decode(&data); err != nil {
132+
return err
133+
}
134+
135+
// Modify YAML
136+
data, err := modFn(data)
137+
if err != nil {
138+
return err
139+
}
140+
141+
// Marshal back to YAML
142+
return yaml.NewEncoder(out).Encode(data)
143+
}

go.mod

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
module github.com/cert-manager/helm-tool
22

3-
go 1.21
3+
go 1.23.0
44

55
require (
66
github.com/Masterminds/sprig/v3 v3.3.0
7+
github.com/google/go-containerregistry v0.20.3
78
github.com/spf13/cobra v1.9.1
89
github.com/stretchr/testify v1.10.0
910
gopkg.in/yaml.v3 v3.0.1
@@ -14,7 +15,11 @@ require (
1415
dario.cat/mergo v1.0.1 // indirect
1516
github.com/Masterminds/goutils v1.1.1 // indirect
1617
github.com/Masterminds/semver/v3 v3.3.0 // indirect
18+
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
1719
github.com/davecgh/go-spew v1.1.1 // indirect
20+
github.com/docker/cli v27.5.0+incompatible // indirect
21+
github.com/docker/distribution v2.8.3+incompatible // indirect
22+
github.com/docker/docker-credential-helpers v0.8.2 // indirect
1823
github.com/go-openapi/jsonpointer v0.19.6 // indirect
1924
github.com/go-openapi/jsonreference v0.20.1 // indirect
2025
github.com/go-openapi/swag v0.22.3 // indirect
@@ -24,13 +29,22 @@ require (
2429
github.com/huandu/xstrings v1.5.0 // indirect
2530
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2631
github.com/josharian/intern v1.0.0 // indirect
32+
github.com/klauspost/compress v1.17.11 // indirect
2733
github.com/mailru/easyjson v0.7.7 // indirect
2834
github.com/mitchellh/copystructure v1.2.0 // indirect
35+
github.com/mitchellh/go-homedir v1.1.0 // indirect
2936
github.com/mitchellh/reflectwalk v1.0.2 // indirect
37+
github.com/opencontainers/go-digest v1.0.0 // indirect
38+
github.com/opencontainers/image-spec v1.1.0 // indirect
39+
github.com/pkg/errors v0.9.1 // indirect
3040
github.com/pmezard/go-difflib v1.0.0 // indirect
3141
github.com/shopspring/decimal v1.4.0 // indirect
42+
github.com/sirupsen/logrus v1.9.3 // indirect
3243
github.com/spf13/cast v1.7.0 // indirect
3344
github.com/spf13/pflag v1.0.6 // indirect
45+
github.com/vbatts/tar-split v0.11.6 // indirect
3446
golang.org/x/crypto v0.26.0 // indirect
35-
google.golang.org/protobuf v1.27.1 // indirect
47+
golang.org/x/sync v0.10.0 // indirect
48+
golang.org/x/sys v0.29.0 // indirect
49+
google.golang.org/protobuf v1.36.3 // indirect
3650
)

0 commit comments

Comments
 (0)