Skip to content

Commit 8b0f62a

Browse files
committed
add Chart image baking
Signed-off-by: Tim Ramlot <[email protected]>
1 parent 2061e8b commit 8b0f62a

File tree

5 files changed

+493
-4
lines changed

5 files changed

+493
-4
lines changed

baker/bake.go

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

baker/modify_values.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Copyright 2025 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package baker
18+
19+
import (
20+
"archive/tar"
21+
"bytes"
22+
"compress/gzip"
23+
"fmt"
24+
"io"
25+
"os"
26+
"strings"
27+
28+
"gopkg.in/yaml.v3"
29+
)
30+
31+
// inplaceReadValuesYAML reads the provided chart tar file and returns the values
32+
func readValuesYAML(inputPath string) (map[string]any, error) {
33+
var result map[string]any
34+
return result, modifyValuesYAML(inputPath, "", func(m map[string]any) (map[string]any, error) {
35+
result = m
36+
return m, nil
37+
})
38+
}
39+
40+
type modFunction func(map[string]any) (map[string]any, error)
41+
42+
func modifyValuesYAML(inFilePath string, outFilePath string, modFn modFunction) error {
43+
inReader, err := os.Open(inFilePath)
44+
if err != nil {
45+
return err
46+
}
47+
defer inReader.Close()
48+
49+
outWriter := io.Discard
50+
if outFilePath != "" {
51+
outFile, err := os.Create(outFilePath)
52+
if err != nil {
53+
return err
54+
}
55+
defer outFile.Close()
56+
57+
outWriter = outFile
58+
}
59+
60+
if strings.HasSuffix(inFilePath, ".tgz") {
61+
if err := modifyTarStreamValuesYAML(inReader, outWriter, modFn); err != nil {
62+
return err
63+
}
64+
} else {
65+
if err := modifyStreamValuesYAML(inReader, outWriter, modFn); err != nil {
66+
return err
67+
}
68+
}
69+
70+
return nil
71+
}
72+
73+
func modifyTarStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
74+
inFileDecompressed, err := gzip.NewReader(in)
75+
if err != nil {
76+
return err
77+
}
78+
defer inFileDecompressed.Close()
79+
tr := tar.NewReader(inFileDecompressed)
80+
81+
outFileCompressed, err := gzip.NewWriterLevel(out, gzip.BestCompression)
82+
if err != nil {
83+
return err
84+
}
85+
// see https://github.com/helm/helm/blob/b25a51b291b930ef05ed0f4a9ea7cce073c10f63/pkg/chart/v2/util/save.go#L35C28-L35C67
86+
// seems like an easter egg, but keep it for "backwards compatibility" :)
87+
outFileCompressed.Extra = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
88+
outFileCompressed.Comment = "Helm"
89+
defer outFileCompressed.Close()
90+
tw := tar.NewWriter(outFileCompressed)
91+
defer tw.Close()
92+
93+
for {
94+
hdr, err := tr.Next()
95+
if err == io.EOF {
96+
break // End of archive
97+
}
98+
if err != nil {
99+
return err
100+
}
101+
102+
const maxValuesYAMLSize = 2 * 1024 * 1024 // 2MB
103+
limitedReader := &io.LimitedReader{
104+
R: tr,
105+
N: maxValuesYAMLSize,
106+
}
107+
108+
if strings.HasSuffix(hdr.Name, "/values.yaml") {
109+
var modifiedContent bytes.Buffer
110+
if err := modifyStreamValuesYAML(limitedReader, &modifiedContent, modFn); err != nil {
111+
return err
112+
}
113+
114+
// Update header size
115+
hdr.Size = int64(modifiedContent.Len())
116+
117+
// Write updated header and content
118+
if err := tw.WriteHeader(hdr); err != nil {
119+
return err
120+
}
121+
if _, err := tw.Write(modifiedContent.Bytes()); err != nil {
122+
return err
123+
}
124+
} else {
125+
// Stream other files unchanged
126+
if err := tw.WriteHeader(hdr); err != nil {
127+
return err
128+
}
129+
if _, err := io.Copy(tw, limitedReader); err != nil {
130+
return err
131+
}
132+
}
133+
134+
if limitedReader.N <= 0 {
135+
return fmt.Errorf("values.yaml is larger than %v bytes", maxValuesYAMLSize)
136+
}
137+
}
138+
139+
return nil
140+
}
141+
142+
func modifyStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
143+
// Parse YAML
144+
var data map[string]any
145+
if err := yaml.NewDecoder(in).Decode(&data); err != nil {
146+
return err
147+
}
148+
149+
// Modify YAML
150+
data, err := modFn(data)
151+
if err != nil {
152+
return err
153+
}
154+
155+
// Marshal back to YAML
156+
return yaml.NewEncoder(out).Encode(data)
157+
}

0 commit comments

Comments
 (0)