Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions tools/helm-tool/baker/bake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
Copyright 2026 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package baker

import (
"context"
"fmt"
"maps"
"slices"
"strings"

"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)

type BakeReference struct {
Repository string
Tag string
Digest string
}

func ParseBakeReference(value string) (bakeInput BakeReference) {
// extract digest from value
if digestRef, err := name.NewDigest(value); err == nil {
bakeInput.Repository = digestRef.Context().String()
bakeInput.Digest = digestRef.DigestStr()
}
// extract tag from value
if tagRef, err := name.NewTag(value); err == nil {
bakeInput.Repository = tagRef.Context().String()
bakeInput.Tag = tagRef.TagStr()
}
return bakeInput
}

func (br BakeReference) Reference() name.Reference {
repo, _ := name.NewRepository(br.Repository)
if br.Digest != "" {
return repo.Digest(br.Digest)
}
return repo.Tag(br.Tag)
}

func (br BakeReference) String() string {
var builder strings.Builder
_, _ = builder.WriteString(br.Repository)
if br.Tag != "" {
_, _ = builder.WriteString(":")
_, _ = builder.WriteString(br.Tag)
}
if br.Digest != "" {
_, _ = builder.WriteString("@")
_, _ = builder.WriteString(br.Digest)
}
return builder.String()
}

type BakeInput = BakeReference

func (bi BakeInput) Find(ctx context.Context) (BakeOutput, error) {
desc, err := remote.Head(bi.Reference(), remote.WithContext(ctx))
if err != nil {
return BakeReference{}, fmt.Errorf("failed to pull %s", bi)
}
return BakeReference{
Repository: bi.Repository,
Digest: desc.Digest.String(),
Tag: bi.Tag,
}, nil
}

type BakeOutput = BakeReference

func Extract(ctx context.Context, inputPath string) (map[string]BakeInput, error) {
results := map[string]BakeInput{}
values, err := readValuesYAML(inputPath)
if err != nil {
return nil, err
}
if _, err := allNestedStringValues(values, nil, func(path []string, value string) (string, error) {
if path[len(path)-1] != "_defaultReference" {
return value, nil
}
bakeInput := ParseBakeReference(value)
if bakeInput == (BakeInput{}) {
return "", fmt.Errorf("invalid _defaultReference value: %q", value)
}
results[strings.Join(path, ".")] = bakeInput
return value, nil
}); err != nil {
return nil, err
}
return results, nil
}

type BakeAction struct {
In BakeInput `json:"in"`
Out BakeOutput `json:"out"`
}

func Bake(ctx context.Context, inputPath string, outputPath string, valuesPaths []string) (map[string]BakeAction, error) {
results := map[string]BakeAction{}
return results, modifyValuesYAML(inputPath, outputPath, func(values map[string]any) (map[string]any, error) {
replacedValuePaths := map[string]struct{}{}
newValues, err := allNestedStringValues(values, nil, func(path []string, value string) (string, error) {
if path[len(path)-1] != "_defaultReference" {
return value, nil
}
bakeInput := ParseBakeReference(value)
if bakeInput == (BakeInput{}) {
return "", fmt.Errorf("invalid _defaultReference value: %q", value)
}
bakeOutput, err := bakeInput.Find(ctx)
if err != nil {
return "", err
}
pathString := strings.Join(path, ".")
replacedValuePaths[pathString] = struct{}{}
results[pathString] = BakeAction{
In: bakeInput,
Out: bakeOutput,
}
return bakeOutput.String(), nil
})
if err != nil {
return nil, err
}
if len(replacedValuePaths) > len(valuesPaths) {
return nil, fmt.Errorf("too many value paths were replaced: %v", slices.Collect(maps.Keys(replacedValuePaths)))
}
for _, valuesPath := range valuesPaths {
if _, ok := replacedValuePaths[valuesPath]; !ok {
return nil, fmt.Errorf("path was not replaced: %s", valuesPath)
}
}
return newValues.(map[string]any), nil
})
}

func allNestedStringValues(object any, path []string, fn func(path []string, value string) (string, error)) (any, error) {
switch t := object.(type) {
case map[string]any:
for key, value := range t {
keyPath := append(path, key)

Check failure on line 156 in tools/helm-tool/baker/bake.go

View workflow job for this annotation

GitHub Actions / verify

appendAssign: append result not assigned to the same slice (gocritic)
if stringValue, ok := value.(string); ok {
newValue, err := fn(slices.Clone(keyPath), stringValue)
if err != nil {
return nil, err
}
t[key] = newValue
} else {
newValue, err := allNestedStringValues(value, keyPath, fn)
if err != nil {
return nil, err
}
t[key] = newValue
}
}
case map[string]string:
for key, stringValue := range t {
keyPath := append(path, key)

Check failure on line 173 in tools/helm-tool/baker/bake.go

View workflow job for this annotation

GitHub Actions / verify

appendAssign: append result not assigned to the same slice (gocritic)
newValue, err := fn(slices.Clone(keyPath), stringValue)
if err != nil {
return nil, err
}
t[key] = newValue
}
case []any:
for i, value := range t {
path = append(path, fmt.Sprintf("%d", i))
newValue, err := allNestedStringValues(value, path, fn)
if err != nil {
return nil, err
}
t[i] = newValue
}
default:
// ignore object
}
return object, nil
}
57 changes: 57 additions & 0 deletions tools/helm-tool/baker/enterprise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
Copyright 2026 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package baker

import (
"fmt"
"strings"
)

type EnterpriseOptions struct {
Registry string
Namespace string
FIPS bool
AllowEU bool
}

func RewriteEnterpriseImages(inputPath string, outputPath string, opts EnterpriseOptions) error {
if opts.Registry != "" && strings.Contains(opts.Registry, "venafi.eu") && !opts.AllowEU {
return fmt.Errorf("enterprise registry %q requires --allow-eu", opts.Registry)
}
return modifyValuesYAML(inputPath, outputPath, func(values map[string]any) (map[string]any, error) {
if opts.Registry != "" {
values["imageRegistry"] = opts.Registry
}
if opts.Namespace != "" {
values["imageNamespace"] = opts.Namespace
}
if !opts.FIPS {
return values, nil
}
newValues, err := allNestedStringValues(values, nil, func(path []string, value string) (string, error) {
if len(path) < 2 || path[len(path)-2] != "image" || path[len(path)-1] != "name" {
return value, nil
}
if value == "" || strings.HasSuffix(value, "-fips") {
return value, nil
}
return value + "-fips", nil
})
if err != nil {
return nil, err
}
return newValues.(map[string]any), nil
})
}
139 changes: 139 additions & 0 deletions tools/helm-tool/baker/modify_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Copyright 2026 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package baker

import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"strings"

"gopkg.in/yaml.v3"
)

// inplaceReadValuesYAML reads the provided chart tar file and returns the values
func readValuesYAML(inputPath string) (map[string]any, error) {
var result map[string]any
return result, modifyValuesYAML(inputPath, "", func(m map[string]any) (map[string]any, error) {
result = m
return m, nil
})
}

type modFunction func(map[string]any) (map[string]any, error)

func modifyValuesYAML(inFilePath string, outFilePath string, modFn modFunction) error {
inReader, err := os.Open(inFilePath)
if err != nil {
return err
}
defer inReader.Close()
outWriter := io.Discard
if outFilePath != "" {
outFile, err := os.Create(outFilePath)
if err != nil {
return err
}
defer outFile.Close()
outWriter = outFile
}
if strings.HasSuffix(inFilePath, ".tgz") {
if err := modifyTarStreamValuesYAML(inReader, outWriter, modFn); err != nil {
return err
}
} else {
if err := modifyStreamValuesYAML(inReader, outWriter, modFn); err != nil {
return err
}
}
return nil
}

func modifyTarStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
inFileDecompressed, err := gzip.NewReader(in)
if err != nil {
return err
}
defer inFileDecompressed.Close()
tr := tar.NewReader(inFileDecompressed)
outFileCompressed, err := gzip.NewWriterLevel(out, gzip.BestCompression)
if err != nil {
return err
}
outFileCompressed.Extra = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
outFileCompressed.Comment = "Helm"
defer outFileCompressed.Close()
tw := tar.NewWriter(outFileCompressed)
defer tw.Close()
for {
hdr, err := tr.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return err
}
const maxValuesYAMLSize = 2 * 1024 * 1024 // 2MB
limitedReader := &io.LimitedReader{
R: tr,
N: maxValuesYAMLSize,
}
if strings.HasSuffix(hdr.Name, "/values.yaml") {
var modifiedContent bytes.Buffer
if err := modifyStreamValuesYAML(limitedReader, &modifiedContent, modFn); err != nil {
return err
}
// Update header size
hdr.Size = int64(modifiedContent.Len())
// Write updated header and content
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write(modifiedContent.Bytes()); err != nil {
return err
}
} else {
// Stream other files unchanged
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := io.Copy(tw, limitedReader); err != nil {
return err
}
}
if limitedReader.N <= 0 {
return fmt.Errorf("values.yaml is larger than %v bytes", maxValuesYAMLSize)
}
}
return nil
}

func modifyStreamValuesYAML(in io.Reader, out io.Writer, modFn modFunction) error {
// Parse YAML
var data map[string]any
if err := yaml.NewDecoder(in).Decode(&data); err != nil {
return err
}
// Modify YAML
data, err := modFn(data)
if err != nil {
return err
}
// Marshal back to YAML
return yaml.NewEncoder(out).Encode(data)
}
Loading
Loading