diff --git a/tools/helm-tool/baker/bake.go b/tools/helm-tool/baker/bake.go new file mode 100644 index 00000000..4d6dd447 --- /dev/null +++ b/tools/helm-tool/baker/bake.go @@ -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) + 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) + 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 +} diff --git a/tools/helm-tool/baker/enterprise.go b/tools/helm-tool/baker/enterprise.go new file mode 100644 index 00000000..9d2e7c18 --- /dev/null +++ b/tools/helm-tool/baker/enterprise.go @@ -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 + }) +} diff --git a/tools/helm-tool/baker/modify_values.go b/tools/helm-tool/baker/modify_values.go new file mode 100644 index 00000000..d2c3954d --- /dev/null +++ b/tools/helm-tool/baker/modify_values.go @@ -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) +} diff --git a/tools/helm-tool/go.mod b/tools/helm-tool/go.mod new file mode 100644 index 00000000..41336167 --- /dev/null +++ b/tools/helm-tool/go.mod @@ -0,0 +1,26 @@ +module github.com/jetstack/preflight/tools/helm-tool + +go 1.25.0 + +require ( + github.com/google/go-containerregistry v0.20.7 + github.com/spf13/cobra v1.10.2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect + github.com/docker/cli v29.0.3+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect +) diff --git a/tools/helm-tool/go.sum b/tools/helm-tool/go.sum new file mode 100644 index 00000000..d81ba328 --- /dev/null +++ b/tools/helm-tool/go.sum @@ -0,0 +1,56 @@ +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= +github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/tools/helm-tool/main.go b/tools/helm-tool/main.go new file mode 100644 index 00000000..271b10e5 --- /dev/null +++ b/tools/helm-tool/main.go @@ -0,0 +1,208 @@ +/* +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 main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + + "github.com/jetstack/preflight/tools/helm-tool/baker" + "github.com/spf13/cobra" +) + +var ( + imagePathsFile string + enterpriseRegistry string + enterpriseNamespace string + allowEU bool + fips bool +) + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +var rootCmd = cobra.Command{ + Use: "helm-tool", + Short: "cert-manager Helm chart tools", +} + +var imagesCmd = cobra.Command{ + Use: "images", + Short: "image-related helpers", +} + +var imagesExtractCmd = cobra.Command{ + Use: "extract", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + inputPath := args[0] + images, err := baker.Extract(context.TODO(), inputPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not extract: %s\n", err) + os.Exit(1) + } + imagePaths := make([]string, 0, len(images)) + for imagePath := range images { + imagePaths = append(imagePaths, imagePath) + } + slices.Sort(imagePaths) + if imagePathsFile == "" { + if err := json.NewEncoder(os.Stdout).Encode(imagePaths); err != nil { + fmt.Fprintf(os.Stderr, "Could not print found images: %s\n", err) + os.Exit(1) + } + return + } + if err := writeJSONFile(imagePathsFile, imagePaths); err != nil { + fmt.Fprintf(os.Stderr, "Could not write --paths file: %s\n", err) + os.Exit(1) + } + }, +} + +var imagesBakeCmd = cobra.Command{ + Use: "bake", + Args: cobra.RangeArgs(1, 2), + Run: func(cmd *cobra.Command, args []string) { + inputPath := args[0] + outputPath := inputPath + if len(args) == 2 { + outputPath = args[1] + } + if len(imagePathsFile) == 0 { + fmt.Fprintf(os.Stderr, "--paths flag not provided.\n") + os.Exit(1) + } + paths, err := readPathsFile(imagePathsFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not read --paths file: %s\n", err) + os.Exit(1) + } + bakeOutputPath, cleanup, err := TempOutputPath(outputPath, inputPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not prepare output: %s\n", err) + os.Exit(1) + } + if cleanup != nil { + defer cleanup() + } + workInput := inputPath + if enterpriseRegistry != "" || enterpriseNamespace != "" || fips { + enterpriseOutput, enterpriseCleanup, err := TempOutputPath("", workInput) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not prepare enterprise output: %s\n", err) + os.Exit(1) + } + if enterpriseCleanup != nil { + defer enterpriseCleanup() + } + opts := baker.EnterpriseOptions{ + Registry: enterpriseRegistry, + Namespace: enterpriseNamespace, + FIPS: fips, + AllowEU: allowEU, + } + if err := baker.RewriteEnterpriseImages(workInput, enterpriseOutput, opts); err != nil { + fmt.Fprintf(os.Stderr, "Could not rewrite enterprise images: %s\n", err) + os.Exit(1) + } + workInput = enterpriseOutput + } + actions, err := baker.Bake(context.TODO(), workInput, bakeOutputPath, paths) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not bake: %s\n", err) + os.Exit(1) + } + if err := replaceOutputPath(bakeOutputPath, outputPath); err != nil { + fmt.Fprintf(os.Stderr, "Could not write output: %s\n", err) + os.Exit(1) + } + for path, action := range actions { + fmt.Fprintf(os.Stderr, "%s: %s -> %s\n", path, action.In, action.Out) + } + }, +} + +func init() { + rootCmd.AddCommand(&imagesCmd) + imagesCmd.AddCommand(&imagesExtractCmd) + imagesCmd.AddCommand(&imagesBakeCmd) + imagesExtractCmd.PersistentFlags().StringVarP(&imagePathsFile, "paths", "p", "", "file containing paths of image._defaultReference in values.yaml (used as check)") + imagesBakeCmd.PersistentFlags().StringVarP(&imagePathsFile, "paths", "p", "", "file containing paths of image._defaultReference in values.yaml (used as check)") + imagesBakeCmd.PersistentFlags().StringVar(&enterpriseRegistry, "enterprise-registry", "", "set imageRegistry to an enterprise registry") + imagesBakeCmd.PersistentFlags().StringVar(&enterpriseNamespace, "enterprise-namespace", "", "set imageNamespace to an enterprise namespace") + imagesBakeCmd.PersistentFlags().BoolVar(&allowEU, "allow-eu", false, "allow venafi.eu registries (guard until the EU registry exists)") + imagesBakeCmd.PersistentFlags().BoolVar(&fips, "fips", false, "append -fips to all image.name values") +} + +func readPathsFile(path string) ([]string, error) { + jsonBlob, err := os.ReadFile(path) + if err != nil { + return nil, err + } + imagePaths := []string{} + if err := json.Unmarshal(jsonBlob, &imagePaths); err != nil { + return nil, err + } + return imagePaths, nil +} + +func writeJSONFile(path string, payload any) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(payload) +} + +func TempOutputPath(outputPath string, inputPath string) (string, func(), error) { + if outputPath != "" && outputPath != inputPath { + return outputPath, nil, nil + } + baseDir := filepath.Dir(inputPath) + tempFile, err := os.CreateTemp(baseDir, "helm-tool-*.tmp") + if err != nil { + return "", nil, err + } + path := tempFile.Name() + if err := tempFile.Close(); err != nil { + return "", nil, err + } + cleanup := func() { + _ = os.Remove(path) + } + return path, cleanup, nil +} + +func replaceOutputPath(tempPath string, outputPath string) error { + if tempPath == outputPath { + return nil + } + if outputPath == "" { + return errors.New("output path is empty") + } + return os.Rename(tempPath, outputPath) +}