diff --git a/.gitignore b/.gitignore index 25afb1b..35ad44c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Custom gitignore rules +.vscode/ bin/ coverage.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e2c607c..ab838d7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,6 +1,10 @@ version: 2 dist: bin +before: + hooks: + - "{{ if and (not .IsSnapshot) (not .Prerelease) }}sed -i -E 's/v[0-9]+\\.[0-9]+\\.[0-9]+(-(rc|alpha|beta)[.0-9]*)?/v{{ .Version }}/g' README.md docs/*.md{{ else }}true{{ end }}" + builds: - main: "{{ .Env.BUILD_PATH }}" binary: >- diff --git a/Dockerfile b/Dockerfile index 5eb0b39..ef7169f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM docker.io/library/alpine:3.23.3@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659 +FROM docker.io/library/alpine:3.23.4@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11 AS base ARG TARGETPLATFORM ARG CMD_NAME @@ -8,3 +8,41 @@ ENV COMMAND_NAME=${CMD_NAME} COPY ${TARGETPLATFORM}/${CMD_NAME} /usr/local/bin/ CMD ["/bin/sh", "-c", "${COMMAND_NAME}"] + +FROM base AS helm3 + +ARG TARGETPLATFORM +ARG HELM3_VERSION=3.20.0 +RUN <<'EOF' + set -eu + case "${TARGETPLATFORM}" in + linux/amd64) ARCH=amd64 ;; + linux/arm64) ARCH=arm64 ;; + linux/arm/v6) ARCH=arm ;; + linux/arm/v7) ARCH=arm ;; + linux/386) ARCH=386 ;; + *) echo "unsupported platform: ${TARGETPLATFORM}" >&2; exit 1 ;; + esac + wget -qO- "https://get.helm.sh/helm-v${HELM3_VERSION}-linux-${ARCH}.tar.gz" \ + | tar xz --strip-components=1 -C /usr/local/bin "linux-${ARCH}/helm" + helm version +EOF + +FROM base AS helm4 + +ARG TARGETPLATFORM +ARG HELM4_VERSION=4.1.1 +RUN <<'EOF' + set -eu + case "${TARGETPLATFORM}" in + linux/amd64) ARCH=amd64 ;; + linux/arm64) ARCH=arm64 ;; + linux/arm/v6) ARCH=arm ;; + linux/arm/v7) ARCH=arm ;; + linux/386) ARCH=386 ;; + *) echo "unsupported platform: ${TARGETPLATFORM}" >&2; exit 1 ;; + esac + wget -qO- "https://get.helm.sh/helm-v${HELM4_VERSION}-linux-${ARCH}.tar.gz" \ + | tar xz --strip-components=1 -C /usr/local/bin "linux-${ARCH}/helm" + helm version +EOF diff --git a/Makefile b/Makefile index e6689e8..b49708e 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ VERSION_MODULE_NAME:= github.com/mia-platform/mlp/v2/pkg/cmd SUPPORTED_PLATFORMS:= linux/386 linux/amd64 linux/arm64 linux/arm/v6 linux/arm/v7 # Default platform for which building the docker image (darwin can run linux images for the same arch) # as SUPPORTED_PLATFORMS it highly depends on which platform are supported by the base image -DEFAULT_DOCKER_PLATFORM:= linux/$(GOARCH)/$(GOARM) +DEFAULT_DOCKER_PLATFORM:= linux/$(GOARCH)$(if $(GOARM),/$(GOARM)) # List of one or more container registries for tagging the resulting docker images CONTAINER_REGISTRIES:= docker.io/miaplatform ghcr.io/mia-platform # The description used on the org.opencontainers.description label of the container diff --git a/README.md b/README.md index a4fbb50..00c22d3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The main subcommands that the tool has are: - `interpolate`: fill placeholders in kubernetes files with the values of ENV variables - `generate`: create files for kubernetes `ConfigMap` and `Secret` based on files and/or ENV values - `deploy`: create and/or update resources in a kubernetes namespace with the intepolated/generated files -- `kustomize`: run kustomize build +- `kustomize`: run kustomize build with optional Helm chart inflation support - `hydrate`: helper to fill kustomization.yml with resources and patches - `completion`: generate the autocompletion @@ -45,6 +45,14 @@ To build the docker image locally run: make docker-build ``` +To build Docker images with Helm included: + +```sh +make docker-build-helm3 # image tagged with -helm3 +make docker-build-helm4 # image tagged with -helm4 and -helm +make docker-build-helm # alias for docker-build-helm4 +``` + ## Testing `mlp` To run the tests use the command: diff --git a/docs/10_overview.md b/docs/10_overview.md index 589a762..c582d0f 100644 --- a/docs/10_overview.md +++ b/docs/10_overview.md @@ -25,8 +25,10 @@ the ability to use different configuration for different runtime environments. with all the files and patches found - `interpolate`: will run through all the files passed and run through a templating function for render the final manifests -- `kustomize`: is the same command of `kustomize build` and can be used if you project is using the kustomize structure - to render the resources to pass to the `interpolate` command +- `kustomize`: is the same command of `kustomize build` and can be used if your project is using the kustomize structure + to render the resources to pass to the `interpolate` command. It supports Helm chart inflation via the + `--enable-helm` flag, along with `--helm-command`, `--helm-api-versions`, `--helm-kube-version`, and + `--load-restrictor` options For more information about the various options available to the various commands you can always run `mlp --help` to see the helpers. @@ -40,3 +42,4 @@ Below, you can find additional documentation for `mlp`: - [Hydration Logic](./40_hydrate.md) - [Interpolatation Template](./50_interpolate.md) - [Filtered Jobs](./60_filtered_job.md) +- [Kustomize](./70_kustomize.md) diff --git a/docs/20_setup.md b/docs/20_setup.md index d129a62..3480128 100644 --- a/docs/20_setup.md +++ b/docs/20_setup.md @@ -73,6 +73,14 @@ If you want to run the cli in its environment or you want to test the cli you ca docker run ghcr.io/mia-platform/mlp:v2.6.0 ``` +If you need Helm support (e.g. for `mlp kustomize --enable-helm`), use one of the Helm-enabled image variants: + +```sh +docker run ghcr.io/mia-platform/mlp:v2.6.0-helm3 # includes Helm 3 +docker run ghcr.io/mia-platform/mlp:v2.6.0-helm4 # includes Helm 4 +docker run ghcr.io/mia-platform/mlp:v2.6.0-helm # alias for -helm4 +``` + ### Windows `mlp` is not directly compatible with Windows, even if you have Go installed compilation on this OS diff --git a/docs/70_kustomize.md b/docs/70_kustomize.md new file mode 100644 index 0000000..39952cc --- /dev/null +++ b/docs/70_kustomize.md @@ -0,0 +1,54 @@ +# Kustomize + +The `kustomize` subcommand builds a set of KRM resources using a `kustomization.yaml` file, equivalent to +running `kustomize build`. It can optionally inflate Helm charts as part of the build. + +## Usage + +```sh +mlp kustomize [DIR] [flags] +``` + +If `DIR` is omitted, the current directory is used. + +## Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `-o`, `--output` | | Write output to the specified file path instead of stdout | +| `--load-restrictor` | `rootOnly` | Set the file loading restrictor: `rootOnly` or `none` | +| `--enable-helm` | `false` | Enable Helm chart inflation | +| `--helm-command` | `helm` | Path or name of the helm binary | +| `--helm-api-versions` | | Kubernetes API versions used for Helm `Capabilities.APIVersions` | +| `--helm-kube-version` | | Kubernetes version used for Helm `Capabilities.KubeVersion` | + +## Examples + +```sh +# Build the current working directory +mlp kustomize + +# Build a specific path +mlp kustomize /home/config/project + +# Save output to a file +mlp kustomize --output /home/config/build-results.yaml + +# Build with Helm chart inflation +mlp kustomize --enable-helm + +# Build with Helm using a specific binary and Kubernetes version +mlp kustomize --enable-helm --helm-command /usr/local/bin/helm --helm-kube-version 1.30.0 +``` + +## Helm Support + +When `--enable-helm` is set, `mlp kustomize` will inflate any Helm charts referenced in your +`kustomization.yaml` using the [Helm chart inflation generator](https://kubectl.docs.kubernetes.io/references/kustomize/builtins/#_helmchartinflationgenerator_). + +The `helm` binary must be available in `PATH` or specified via `--helm-command`. When using the +Docker image, use one of the Helm-enabled variants: + +- `ghcr.io/mia-platform/mlp:v2.6.0-helm3` — includes Helm 3 +- `ghcr.io/mia-platform/mlp:v2.6.0-helm4` — includes Helm 4 +- `ghcr.io/mia-platform/mlp:v2.6.0-helm` — alias for `-helm4` diff --git a/go.mod b/go.mod index f926f74..90208e5 100644 --- a/go.mod +++ b/go.mod @@ -21,8 +21,8 @@ require ( k8s.io/client-go v0.34.3 k8s.io/utils v0.0.0-20260108192941-914a6e750570 sigs.k8s.io/e2e-framework v0.6.0 - sigs.k8s.io/kustomize/api v0.21.0 - sigs.k8s.io/kustomize/kyaml v0.21.0 + sigs.k8s.io/kustomize/api v0.21.1 + sigs.k8s.io/kustomize/kyaml v0.21.1 sigs.k8s.io/yaml v1.6.0 ) @@ -60,7 +60,6 @@ require ( github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -82,7 +81,7 @@ require ( golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.34.3 // indirect diff --git a/go.sum b/go.sum index 930a887..e412f68 100644 --- a/go.sum +++ b/go.sum @@ -214,8 +214,8 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -245,10 +245,10 @@ sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.21.0 h1:I7nry5p8iDJbuRdYS7ez8MUvw7XVNPcIP5GkzzuXIIQ= -sigs.k8s.io/kustomize/api v0.21.0/go.mod h1:XGVQuR5n2pXKWbzXHweZU683pALGw/AMVO4zU4iS8SE= -sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ= -sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= diff --git a/pkg/cmd/kustomize/kustomize.go b/pkg/cmd/kustomize/kustomize.go index 8b36d3b..686741e 100644 --- a/pkg/cmd/kustomize/kustomize.go +++ b/pkg/cmd/kustomize/kustomize.go @@ -18,6 +18,7 @@ package kustomize import ( "context" "errors" + "fmt" "io" "github.com/MakeNowJust/heredoc/v2" @@ -25,6 +26,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "sigs.k8s.io/kustomize/api/krusty" + "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filesys" ) @@ -50,20 +52,36 @@ const ( outputFlagShort = "o" outputFlagUsage = "If specified, write output to the file at this path" outputIsADirectoryError = "output path is a directory instead of a file" + + enableHelmFlagName = "enable-helm" + helmCommandFlagName = "helm-command" + helmAPIVersionsName = "helm-api-versions" + helmKubeVersionName = "helm-kube-version" + loadRestrictorName = "load-restrictor" ) // Flags contains all the flags for the `kustomize` command. They will be converted to Options // that contains all runtime options for the command. type Flags struct { - outputPath string + outputPath string + enableHelm bool + helmCommand string + helmAPIVersions []string + helmKubeVersion string + loadRestrictor string } // Options have the data required to perform the kustomize operation type Options struct { - inputPath string - outputPath string - fSys filesys.FileSystem - writer io.Writer + inputPath string + outputPath string + fSys filesys.FileSystem + enableHelm bool + helmCommand string + helmAPIVersions []string + helmKubeVersion string + loadRestrictor string + writer io.Writer } // NewCommand return the command for build a kustomization target from a directory @@ -92,6 +110,11 @@ func NewCommand() *cobra.Command { // AddFlags set the connection between Flags property to command line flags func (f *Flags) AddFlags(set *pflag.FlagSet) { set.StringVarP(&f.outputPath, outputFlagName, outputFlagShort, "", outputFlagUsage) + set.BoolVar(&f.enableHelm, enableHelmFlagName, false, "enable Helm chart inflation") + set.StringVar(&f.helmCommand, helmCommandFlagName, "helm", "path or name of the helm binary") + set.StringSliceVar(&f.helmAPIVersions, helmAPIVersionsName, nil, "Kubernetes api versions used for Helm Capabilities.APIVersions") + set.StringVar(&f.helmKubeVersion, helmKubeVersionName, "", "Kubernetes version used for Helm Capabilities.KubeVersion") + set.StringVar(&f.loadRestrictor, loadRestrictorName, "rootOnly", `set the file loading restrictor: "rootOnly" or "none"`) } // ToOptions transform the command flags in command runtime arguments @@ -109,10 +132,15 @@ func (f *Flags) ToOptions(args []string, fSys filesys.FileSystem, writer io.Writ } return &Options{ - inputPath: inputPath, - outputPath: f.outputPath, - fSys: fSys, - writer: writer, + inputPath: inputPath, + outputPath: f.outputPath, + fSys: fSys, + enableHelm: f.enableHelm, + helmCommand: f.helmCommand, + helmAPIVersions: f.helmAPIVersions, + helmKubeVersion: f.helmKubeVersion, + loadRestrictor: f.loadRestrictor, + writer: writer, }, nil } @@ -121,7 +149,29 @@ func (o *Options) Run(ctx context.Context) error { logger := logr.FromContextOrDiscard(ctx) logger.V(5).Info("reading kustomize files", "path", o.inputPath) - kustomizer := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) + opts := krusty.MakeDefaultOptions() + + if o.enableHelm { + opts.PluginConfig.HelmConfig.Enabled = true + opts.PluginConfig.HelmConfig.Command = o.helmCommand + if len(o.helmAPIVersions) > 0 { + opts.PluginConfig.HelmConfig.ApiVersions = o.helmAPIVersions + } + if o.helmKubeVersion != "" { + opts.PluginConfig.HelmConfig.KubeVersion = o.helmKubeVersion + } + } + + switch o.loadRestrictor { + case "none": + opts.LoadRestrictions = types.LoadRestrictionsNone + case "rootOnly": + opts.LoadRestrictions = types.LoadRestrictionsRootOnly + default: + return fmt.Errorf("invalid load-restrictor value %q: must be \"rootOnly\" or \"none\"", o.loadRestrictor) + } + + kustomizer := krusty.MakeKustomizer(opts) resourceMap, err := kustomizer.Run(o.fSys, o.inputPath) if err != nil { return err diff --git a/pkg/cmd/kustomize/kustomize_test.go b/pkg/cmd/kustomize/kustomize_test.go index bae6778..481e460 100644 --- a/pkg/cmd/kustomize/kustomize_test.go +++ b/pkg/cmd/kustomize/kustomize_test.go @@ -21,6 +21,7 @@ import ( "path/filepath" "testing" + "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/kyaml/filesys" @@ -40,6 +41,76 @@ func TestCommand(t *testing.T) { buffer.Reset() } +func TestCommandWithFlags(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + args []string + expectError bool + }{ + "with load-restrictor none": { + args: []string{"--load-restrictor", "none", "testdata"}, + }, + "with enable-helm": { + args: []string{"--enable-helm", "testdata"}, + }, + "with output flag": { + args: []string{"-o", filepath.Join(t.TempDir(), "out.yaml"), "testdata"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + cmd := NewCommand() + buffer := new(bytes.Buffer) + cmd.SetArgs(test.args) + cmd.SetOut(buffer) + cmd.SetErr(buffer) + err := cmd.Execute() + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestAddFlags(t *testing.T) { + t.Parallel() + + flags := &Flags{} + set := pflag.NewFlagSet("test", pflag.ContinueOnError) + flags.AddFlags(set) + + // verify defaults + assert.Empty(t, flags.outputPath) + assert.False(t, flags.enableHelm) + assert.Equal(t, "helm", flags.helmCommand) + assert.Nil(t, flags.helmAPIVersions) + assert.Empty(t, flags.helmKubeVersion) + assert.Equal(t, "rootOnly", flags.loadRestrictor) + + // verify parsing + err := set.Parse([]string{ + "--enable-helm", + "--helm-command", "/usr/local/bin/helm", + "--helm-api-versions", "v1,apps/v1", + "--helm-kube-version", "1.30.0", + "--load-restrictor", "none", + "-o", "/tmp/out.yaml", + }) + require.NoError(t, err) + assert.True(t, flags.enableHelm) + assert.Equal(t, "/usr/local/bin/helm", flags.helmCommand) + assert.Equal(t, []string{"v1", "apps/v1"}, flags.helmAPIVersions) + assert.Equal(t, "1.30.0", flags.helmKubeVersion) + assert.Equal(t, "none", flags.loadRestrictor) + assert.Equal(t, "/tmp/out.yaml", flags.outputPath) +} + func TestToOptions(t *testing.T) { t.Parallel() @@ -82,6 +153,28 @@ func TestToOptions(t *testing.T) { }, expectedError: outputIsADirectoryError, }, + "all fields propagated": { + flags: &Flags{ + outputPath: filepath.Join(testPath, "file.txt"), + enableHelm: true, + helmCommand: "/usr/local/bin/helm", + helmAPIVersions: []string{"v1", "apps/v1"}, + helmKubeVersion: "1.30.0", + loadRestrictor: "none", + }, + args: []string{"input"}, + expectedOptions: &Options{ + outputPath: filepath.Join(testPath, "file.txt"), + inputPath: "input", + enableHelm: true, + helmCommand: "/usr/local/bin/helm", + helmAPIVersions: []string{"v1", "apps/v1"}, + helmKubeVersion: "1.30.0", + loadRestrictor: "none", + fSys: fSys, + writer: buffer, + }, + }, } for name, test := range tests { @@ -111,36 +204,124 @@ func TestRun(t *testing.T) { }{ "run correctly": { options: &Options{ - inputPath: "testdata", - outputPath: "", - fSys: filesys.MakeFsOnDisk(), - writer: new(bytes.Buffer), + inputPath: "testdata", + outputPath: "", + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "rootOnly", }, }, "run saving on file": { options: &Options{ - inputPath: "testdata", - outputPath: filepath.Join(t.TempDir(), "output.yaml"), - fSys: filesys.MakeFsOnDisk(), + inputPath: "testdata", + outputPath: filepath.Join(t.TempDir(), "output.yaml"), + fSys: filesys.MakeFsOnDisk(), + loadRestrictor: "rootOnly", }, }, "error reading files": { options: &Options{ - inputPath: "testdata", - outputPath: "", - fSys: filesys.MakeEmptyDirInMemory(), + inputPath: "testdata", + outputPath: "", + fSys: filesys.MakeEmptyDirInMemory(), + loadRestrictor: "rootOnly", }, expectedError: "not a valid directory", }, "error during writes": { options: &Options{ - inputPath: "testdata", - outputPath: "", - fSys: filesys.MakeFsOnDisk(), - writer: failWriter{}, + inputPath: "testdata", + outputPath: "", + fSys: filesys.MakeFsOnDisk(), + writer: failWriter{}, + loadRestrictor: "rootOnly", }, expectedError: "nope", }, + "run with load restrictor none": { + options: &Options{ + inputPath: "testdata", + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "none", + }, + }, + "error for invalid load restrictor": { + options: &Options{ + inputPath: "testdata", + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "invalid", + }, + expectedError: `invalid load-restrictor value "invalid"`, + }, + "error for empty load restrictor": { + options: &Options{ + inputPath: "testdata", + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "", + }, + expectedError: `invalid load-restrictor value ""`, + }, + "enable helm without charts is no-op": { + options: &Options{ + inputPath: "testdata", + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + enableHelm: true, + helmCommand: "helm", + loadRestrictor: "rootOnly", + }, + }, + "cross-directory with rootOnly fails": { + options: &Options{ + inputPath: filepath.Join("testdata", "cross-directory"), + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "rootOnly", + }, + expectedError: "is not in or below", + }, + "cross-directory with none succeeds": { + options: &Options{ + inputPath: filepath.Join("testdata", "cross-directory"), + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "none", + }, + expectedOutput: "shared-config", + }, + "with-resources produces output": { + options: &Options{ + inputPath: filepath.Join("testdata", "with-resources"), + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + loadRestrictor: "rootOnly", + }, + expectedOutput: "test-config", + }, + "helm chart inflation with fake helm": { + options: &Options{ + inputPath: filepath.Join("testdata", "with-helm"), + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + enableHelm: true, + helmCommand: fakeHelmPath(t), + loadRestrictor: "rootOnly", + }, + expectedOutput: "from-helm-chart", + }, + "helm chart inflation disabled fails": { + options: &Options{ + inputPath: filepath.Join("testdata", "with-helm"), + fSys: filesys.MakeFsOnDisk(), + writer: new(bytes.Buffer), + enableHelm: false, + loadRestrictor: "rootOnly", + }, + expectedError: "must specify --enable-helm", + }, } for name, test := range tests { @@ -154,6 +335,12 @@ func TestRun(t *testing.T) { default: assert.ErrorContains(t, err, test.expectedError) } + + if test.expectedOutput != "" { + if buf, ok := test.options.writer.(*bytes.Buffer); ok { + assert.Contains(t, buf.String(), test.expectedOutput) + } + } }) } } @@ -163,3 +350,10 @@ type failWriter struct{} // Write implements the Writer interface's Write method and returns an error. func (failWriter) Write([]byte) (int, error) { return 0, errors.New("nope") } + +func fakeHelmPath(t *testing.T) string { + t.Helper() + absPath, err := filepath.Abs(filepath.Join("testdata", "with-helm", "fake-helm.sh")) + require.NoError(t, err) + return absPath +} diff --git a/pkg/cmd/kustomize/testdata/cross-directory/kustomization.yaml b/pkg/cmd/kustomize/testdata/cross-directory/kustomization.yaml new file mode 100644 index 0000000..6d26aff --- /dev/null +++ b/pkg/cmd/kustomize/testdata/cross-directory/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../shared/configmap.yaml diff --git a/pkg/cmd/kustomize/testdata/shared/configmap.yaml b/pkg/cmd/kustomize/testdata/shared/configmap.yaml new file mode 100644 index 0000000..375d0e0 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/shared/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: shared-config +data: + key: value diff --git a/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/Chart.yaml b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/Chart.yaml new file mode 100644 index 0000000..67519a4 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: test-chart +version: 0.1.0 +description: A minimal test chart diff --git a/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/templates/configmap.yaml b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/templates/configmap.yaml new file mode 100644 index 0000000..7755a39 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-config +data: + chart: {{ .Chart.Name }} diff --git a/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/values.yaml b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/values.yaml new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-helm/charts/test-chart/values.yaml @@ -0,0 +1 @@ +{} diff --git a/pkg/cmd/kustomize/testdata/with-helm/fake-helm.sh b/pkg/cmd/kustomize/testdata/with-helm/fake-helm.sh new file mode 100755 index 0000000..36c6720 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-helm/fake-helm.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Mock helm binary for testing - outputs a fixed ConfigMap +case "$1" in + template) + cat <<'EOF' +apiVersion: v1 +kind: ConfigMap +metadata: + name: from-helm-chart +data: + rendered: "true" +EOF + ;; + version) + echo "v3.20.0+mock" + ;; + *) + echo "mock helm: unsupported command $1" >&2 + exit 1 + ;; +esac diff --git a/pkg/cmd/kustomize/testdata/with-helm/kustomization.yaml b/pkg/cmd/kustomize/testdata/with-helm/kustomization.yaml new file mode 100644 index 0000000..44ad556 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-helm/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmGlobals: + chartHome: charts +helmCharts: +- name: test-chart + releaseName: test-release + version: 0.1.0 diff --git a/pkg/cmd/kustomize/testdata/with-resources/configmap.yaml b/pkg/cmd/kustomize/testdata/with-resources/configmap.yaml new file mode 100644 index 0000000..45afe67 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-resources/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-config +data: + app.env: production diff --git a/pkg/cmd/kustomize/testdata/with-resources/kustomization.yaml b/pkg/cmd/kustomize/testdata/with-resources/kustomization.yaml new file mode 100644 index 0000000..f563639 --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-resources/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- configmap.yaml +- secret.yaml diff --git a/pkg/cmd/kustomize/testdata/with-resources/secret.yaml b/pkg/cmd/kustomize/testdata/with-resources/secret.yaml new file mode 100644 index 0000000..daf975c --- /dev/null +++ b/pkg/cmd/kustomize/testdata/with-resources/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-secret +type: Opaque +data: + example-key: dGVzdC12YWx1ZQ== diff --git a/tools/make/build.mk b/tools/make/build.mk index 5efb634..474f31a 100644 --- a/tools/make/build.mk +++ b/tools/make/build.mk @@ -73,7 +73,7 @@ build-multiarch: $(GORELEASER_PATH) go/build/multiarch endif .PHONY: build -build: go/build/$(GOOS)/$(GOARCH)/$(GOARM) +build: go/build/$(GOOS)/$(GOARCH)$(if $(GOARM),/$(GOARM)) $(TOOLS_BIN)/goreleaser: $(TOOLS_DIR)/GORELEASER_VERSION $(eval GORELEASER_VERSION:= $(shell cat $<)) diff --git a/tools/make/container.mk b/tools/make/container.mk index 463e6c8..ebb1c29 100644 --- a/tools/make/container.mk +++ b/tools/make/container.mk @@ -54,24 +54,32 @@ docker/%/multiarch: $(eval ACTION:= $(word 1,$(subst /, , $*))) $(eval IS_PUSH:= $(filter push,$(ACTION))) $(eval ADDITIONAL_PARAMETER:= $(if $(IS_PUSH), --push)) - $(info Building image for following platforms: $(SUPPORTED_PLATFORMS)) + $(info Building image for following platforms: $(SUPPORTED_PLATFORMS) [target: $(DOCKER_TARGET)]) $(DOCKER_CMD) buildx build --platform "$(DOCKER_SUPPORTED_PLATFORMS)" \ --build-arg CMD_NAME=$(CMDNAME) \ --provenance=false \ - $(IMAGE_TAGS) \ + $(DOCKER_TARGET_FLAG) \ + $(DOCKER_TARGET_IMAGE_TAGS) \ $(DOCKER_LABELS) \ $(DOCKER_ANNOTATIONS) \ --file ./Dockerfile $(OUTPUT_DIR) $(ADDITIONAL_PARAMETER) +DOCKER_TARGET?= base +DOCKER_TARGET_FLAG= $(if $(DOCKER_TARGET),--target $(DOCKER_TARGET)) +DOCKER_TARGET_SUFFIX= $(if $(filter-out base,$(DOCKER_TARGET)),-$(DOCKER_TARGET)) +DOCKER_HELM_ALIAS_TAGS= $(if $(filter helm4,$(DOCKER_TARGET)),$(addprefix --tag , $(foreach REGISTRY, $(CONTAINER_REGISTRIES), $(foreach TAG, $(PARSED_TAGS), $(REGISTRY)/$(CMDNAME):$(TAG)-helm)))) +DOCKER_TARGET_IMAGE_TAGS= $(addprefix --tag , $(foreach REGISTRY, $(CONTAINER_REGISTRIES), $(foreach TAG, $(PARSED_TAGS), $(REGISTRY)/$(CMDNAME):$(TAG)$(DOCKER_TARGET_SUFFIX)))) $(DOCKER_HELM_ALIAS_TAGS) + .PHONY: docker/build/% docker/build/%: $(eval OS:= $(word 1,$(subst /, ,$*))) $(eval ARCH:= $(word 2,$(subst /, ,$*))) $(eval ARM:= $(word 3,$(subst /, ,$*))) - $(info Building image for $(OS) $(ARCH) $(ARM)) + $(info Building image for $(OS) $(ARCH) $(ARM) [target: $(DOCKER_TARGET)]) $(DOCKER_CMD) build --platform $* \ --build-arg CMD_NAME=$(CMDNAME) \ - $(IMAGE_TAGS) \ + $(DOCKER_TARGET_FLAG) \ + $(DOCKER_TARGET_IMAGE_TAGS) \ $(DOCKER_LABELS) \ $(DOCKER_ANNOTATIONS) \ --file ./Dockerfile $(OUTPUT_DIR) @@ -92,11 +100,35 @@ docker/buildx/teardown: .PHONY: docker-build docker-build: go/build/$(DEFAULT_DOCKER_PLATFORM) docker/build/$(DEFAULT_DOCKER_PLATFORM) +.PHONY: docker-build-helm3 +docker-build-helm3: DOCKER_TARGET=helm3 +docker-build-helm3: docker-build + +.PHONY: docker-build-helm4 +docker-build-helm4: DOCKER_TARGET=helm4 +docker-build-helm4: docker-build + +.PHONY: docker-build-helm +docker-build-helm: docker-build-helm4 + .PHONY: docker-setup-multiarch docker-setup-multiarch: docker/setup/multiarch .PHONY: docker-build-multiarch docker-build-multiarch: build-multiarch docker/buildx/setup docker/build/multiarch docker/buildx/teardown +.PHONY: docker-build-multiarch-helm3 +docker-build-multiarch-helm3: DOCKER_TARGET=helm3 +docker-build-multiarch-helm3: docker-build-multiarch + +.PHONY: docker-build-multiarch-helm4 +docker-build-multiarch-helm4: DOCKER_TARGET=helm4 +docker-build-multiarch-helm4: docker-build-multiarch + +.PHONY: docker-build-multiarch-helm +docker-build-multiarch-helm: docker-build-multiarch-helm4 + .PHONY: ci-docker ci-docker: docker/push/multiarch + $(MAKE) docker/push/multiarch DOCKER_TARGET=helm3 + $(MAKE) docker/push/multiarch DOCKER_TARGET=helm4