Skip to content

Commit 5b160f6

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

File tree

5 files changed

+515
-4
lines changed

5 files changed

+515
-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, valuesPaths []string) (map[string]BakeAction, error) {
125+
results := map[string]BakeAction{}
126+
return results, inplaceModifyValuesYAML(inputPath, 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: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
"errors"
24+
"fmt"
25+
"io"
26+
"os"
27+
"strings"
28+
29+
"gopkg.in/yaml.v3"
30+
)
31+
32+
// inplaceModifyValuesYAML modifies the provided chart tar file and updates the values.yaml file.
33+
func inplaceModifyValuesYAML(inputPath string, modFn func(map[string]any) (map[string]any, error)) error {
34+
tempOut := fmt.Sprintf("%s.tmp", inputPath)
35+
36+
if err := modifyValuesYAML(inputPath, tempOut, modFn); err != nil {
37+
return errors.Join(err, os.Remove(tempOut))
38+
} else {
39+
return os.Rename(tempOut, inputPath)
40+
}
41+
}
42+
43+
// inplaceReadValuesYAML reads the provided chart tar file and returns the values
44+
func readValuesYAML(inputPath string) (map[string]any, error) {
45+
var result map[string]any
46+
return result, modifyValuesYAML(inputPath, "", func(m map[string]any) (map[string]any, error) {
47+
result = m
48+
return m, nil
49+
})
50+
}
51+
52+
type modFunction func(map[string]any) (map[string]any, error)
53+
54+
func modifyValuesYAML(inFilePath string, outFilePath string, modFn modFunction) error {
55+
inReader, err := os.Open(inFilePath)
56+
if err != nil {
57+
return err
58+
}
59+
defer inReader.Close()
60+
61+
outWriter := io.Discard
62+
if outFilePath != "" {
63+
outFile, err := os.Create(outFilePath)
64+
if err != nil {
65+
return err
66+
}
67+
defer outFile.Close()
68+
69+
outWriter = outFile
70+
}
71+
72+
if strings.HasSuffix(inFilePath, ".tgz") {
73+
if err := modifyTarStreamValuesYAML(inReader, outWriter, modFn); err != nil {
74+
return err
75+
}
76+
} else {
77+
if err := modifyStreamValuesYAML(inReader, outWriter, modFn); err != nil {
78+
return err
79+
}
80+
}
81+
82+
return nil
83+
}
84+
85+
func modifyTarStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
86+
inFileDecompressed, err := gzip.NewReader(in)
87+
if err != nil {
88+
return err
89+
}
90+
defer inFileDecompressed.Close()
91+
tr := tar.NewReader(inFileDecompressed)
92+
93+
outFileCompressed, err := gzip.NewWriterLevel(out, gzip.BestCompression)
94+
if err != nil {
95+
return err
96+
}
97+
// see https://github.com/helm/helm/blob/b25a51b291b930ef05ed0f4a9ea7cce073c10f63/pkg/chart/v2/util/save.go#L35C28-L35C67
98+
// seems like an easter egg, but keep it for "backwards compatibility" :)
99+
outFileCompressed.Extra = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
100+
outFileCompressed.Comment = "Helm"
101+
defer outFileCompressed.Close()
102+
tw := tar.NewWriter(outFileCompressed)
103+
defer tw.Close()
104+
105+
for {
106+
hdr, err := tr.Next()
107+
if err == io.EOF {
108+
break // End of archive
109+
}
110+
if err != nil {
111+
return err
112+
}
113+
114+
const maxValuesYAMLSize = 2 * 1024 * 1024 // 2MB
115+
limitedReader := &io.LimitedReader{
116+
R: tr,
117+
N: maxValuesYAMLSize,
118+
}
119+
120+
if strings.HasSuffix(hdr.Name, "/values.yaml") {
121+
var modifiedContent bytes.Buffer
122+
if err := modifyStreamValuesYAML(limitedReader, &modifiedContent, modFn); err != nil {
123+
return err
124+
}
125+
126+
// Update header size
127+
hdr.Size = int64(modifiedContent.Len())
128+
129+
// Write updated header and content
130+
if err := tw.WriteHeader(hdr); err != nil {
131+
return err
132+
}
133+
if _, err := tw.Write(modifiedContent.Bytes()); err != nil {
134+
return err
135+
}
136+
} else {
137+
// Stream other files unchanged
138+
if err := tw.WriteHeader(hdr); err != nil {
139+
return err
140+
}
141+
if _, err := io.Copy(tw, limitedReader); err != nil {
142+
return err
143+
}
144+
}
145+
146+
if limitedReader.N <= 0 {
147+
return fmt.Errorf("values.yaml is larger than %v bytes", maxValuesYAMLSize)
148+
}
149+
}
150+
151+
return nil
152+
}
153+
154+
func modifyStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
155+
// Parse YAML
156+
var data map[string]any
157+
if err := yaml.NewDecoder(in).Decode(&data); err != nil {
158+
return err
159+
}
160+
161+
// Modify YAML
162+
data, err := modFn(data)
163+
if err != nil {
164+
return err
165+
}
166+
167+
// Marshal back to YAML
168+
return yaml.NewEncoder(out).Encode(data)
169+
}

0 commit comments

Comments
 (0)