Skip to content
Open
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
109 changes: 109 additions & 0 deletions kubectl_kustomize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package chartify

import (
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
)

// TestKubectlKustomize tests behavior when kubectl kustomize is explicitly configured
// via KustomizeBin("kubectl kustomize"). The automatic fallback selection is tested
// in TestKustomizeBin.
func TestKubectlKustomize(t *testing.T) {
t.Run("KustomizeBuild succeeds with kubectl kustomize option", func(t *testing.T) {
if _, err := exec.LookPath("kubectl"); err != nil {
t.Skip("kubectl binary not found in PATH")
}
Comment on lines +16 to +19
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test will be skipped on systems without kubectl in PATH. The repo CI workflow installs kustomize but not kubectl (.github/workflows/go.yml), so this coverage is likely skipped in CI and won’t protect the new behavior. Consider stubbing a minimal kubectl script in a temp PATH (similar to TestKustomizeBin) so the test always exercises the kubectl kustomize code path.

Copilot uses AI. Check for mistakes.

tmpDir := t.TempDir()
srcDir := t.TempDir()

kustomizationContent := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
`
deploymentContent := `apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
replicas: 1
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: test:latest
`

templatesDir := filepath.Join(tmpDir, "templates")
require.NoError(t, os.MkdirAll(templatesDir, 0755))

require.NoError(t, os.WriteFile(filepath.Join(srcDir, "kustomization.yaml"), []byte(kustomizationContent), 0644))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "deployment.yaml"), []byte(deploymentContent), 0644))

r := New(KustomizeBin("kubectl kustomize"))

outputFile, err := r.KustomizeBuild(srcDir, tmpDir)
require.NoError(t, err)
require.FileExists(t, outputFile)
})

t.Run("edit commands not supported with kubectl kustomize", func(t *testing.T) {
tmpDir := t.TempDir()
srcDir := t.TempDir()

kustomizationContent := `apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
`
deploymentContent := `apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
replicas: 1
selector:
matchLabels:
app: test
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: test:latest
`

templatesDir := filepath.Join(tmpDir, "templates")
valuesDir := t.TempDir()
valuesFile := filepath.Join(valuesDir, "values.yaml")
valuesContent := `images:
- name: test
newName: newtest
newTag: v2
`

require.NoError(t, os.MkdirAll(templatesDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "kustomization.yaml"), []byte(kustomizationContent), 0644))
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "deployment.yaml"), []byte(deploymentContent), 0644))
require.NoError(t, os.WriteFile(valuesFile, []byte(valuesContent), 0644))

r := New(KustomizeBin("kubectl kustomize"))

_, err := r.KustomizeBuild(srcDir, tmpDir, &KustomizeBuildOpts{ValuesFiles: []string{valuesFile}})
require.Error(t, err)
require.Contains(t, err.Error(), "setting images via kustomizeOpts.Images is not supported when using 'kubectl kustomize'")
})
}
29 changes: 28 additions & 1 deletion kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize
}

if len(kustomizeOpts.Images) > 0 {
if r.isUsingKubectlKustomize() {
return "", fmt.Errorf("setting images via kustomizeOpts.Images is not supported when using 'kubectl kustomize'. Please set images directly in your kustomization.yaml file")
}
args := []string{"edit", "set", "image"}
for _, image := range kustomizeOpts.Images {
args = append(args, image.String())
Expand All @@ -120,27 +123,40 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize
}
}
if kustomizeOpts.NamePrefix != "" {
if r.isUsingKubectlKustomize() {
return "", fmt.Errorf("setting namePrefix via kustomizeOpts.NamePrefix is not supported when using 'kubectl kustomize'. Please set namePrefix directly in your kustomization.yaml file")
}
_, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "nameprefix", kustomizeOpts.NamePrefix)
if err != nil {
fmt.Println(err)
return "", err
}
}
if kustomizeOpts.NameSuffix != "" {
if r.isUsingKubectlKustomize() {
return "", fmt.Errorf("setting nameSuffix via kustomizeOpts.NameSuffix is not supported when using 'kubectl kustomize'. Please set nameSuffix directly in your kustomization.yaml file")
}
// "--" is there to avoid `namesuffix -acme` to fail due to `-a` being considered as a flag
_, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "namesuffix", "--", kustomizeOpts.NameSuffix)
if err != nil {
return "", err
}
}
if kustomizeOpts.Namespace != "" {
if r.isUsingKubectlKustomize() {
return "", fmt.Errorf("setting namespace via kustomizeOpts.Namespace is not supported when using 'kubectl kustomize'. Please set namespace directly in your kustomization.yaml file")
}
_, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "namespace", kustomizeOpts.Namespace)
if err != nil {
return "", err
}
}
outputFile := filepath.Join(tempDir, "templates", "kustomized.yaml")
kustomizeArgs := []string{"-o", outputFile, "build"}
kustomizeArgs := []string{"-o", outputFile}

if !r.isUsingKubectlKustomize() {
kustomizeArgs = append(kustomizeArgs, "build")
}

if u.EnableAlphaPlugins {
f, err := r.kustomizeEnableAlphaPluginsFlag()
Expand Down Expand Up @@ -190,10 +206,18 @@ func (r *Runner) kustomizeVersion() (*semver.Version, error) {
return version, nil
}

// isUsingKubectlKustomize checks if we're using kubectl's built-in kustomize
func (r *Runner) isUsingKubectlKustomize() bool {
return r.kustomizeBin() == "kubectl kustomize"
}

// kustomizeEnableAlphaPluginsFlag returns the kustomize binary alpha plugin argument.
// Above Kustomize v3, it is `--enable-alpha-plugins`.
// Below Kustomize v3 (including v3), it is `--enable_alpha_plugins`.
func (r *Runner) kustomizeEnableAlphaPluginsFlag() (string, error) {
if r.isUsingKubectlKustomize() {
return "--enable-alpha-plugins", nil
}
version, err := r.kustomizeVersion()
if err != nil {
return "", err
Expand All @@ -209,6 +233,9 @@ func (r *Runner) kustomizeEnableAlphaPluginsFlag() (string, error) {
// Above Kustomize v3, it is `--load-restrictor=LoadRestrictionsNone`.
// Below Kustomize v3 (including v3), it is `--load_restrictor=none`.
func (r *Runner) kustomizeLoadRestrictionsNoneFlag() (string, error) {
if r.isUsingKubectlKustomize() {
return "--load-restrictor=LoadRestrictionsNone", nil
}
version, err := r.kustomizeVersion()
if err != nil {
return "", err
Expand Down
8 changes: 6 additions & 2 deletions patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,13 @@ resources:

renderedFileName := "all.patched.yaml"
renderedFile := filepath.Join(tempDir, renderedFileName)
r.Logf("Generating %s", renderedFile)
r.Logf("Generating %s", renderedFileName)

kustomizeArgs := []string{"build", tempDir, "--output", renderedFile}
kustomizeArgs := []string{"--output", renderedFile}

if !r.isUsingKubectlKustomize() {
kustomizeArgs = append([]string{"build", tempDir}, kustomizeArgs...)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using the kubectl-based kustomize path, this command invocation never specifies the target directory and also uses r.run (dir=""). That means it will run kubectl kustomize against the process working directory rather than tempDir, producing wrong output or failing. Pass tempDir as the kustomize target (or run in tempDir) for the kubectl case as well, similar to how the non-kubectl path includes build tempDir.

Suggested change
kustomizeArgs = append([]string{"build", tempDir}, kustomizeArgs...)
kustomizeArgs = append([]string{"build", tempDir}, kustomizeArgs...)
} else {
// When using kubectl kustomize, explicitly pass tempDir as the target directory.
kustomizeArgs = append([]string{tempDir}, kustomizeArgs...)

Copilot uses AI. Check for mistakes.
}

if u.EnableAlphaPlugins {
f, err := r.kustomizeEnableAlphaPluginsFlag()
Expand Down
15 changes: 12 additions & 3 deletions runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ func (r *Runner) kustomizeBin() string {
if r.KustomizeBinary != "" {
return r.KustomizeBinary
}
if env := os.Getenv("KUSTOMIZE_BIN"); env != "" {
return env
}
if _, err := exec.LookPath("kustomize"); err == nil {
return "kustomize"
}
if _, err := exec.LookPath("kubectl"); err == nil {
return "kubectl kustomize"
}
return "kustomize"
}

Expand Down Expand Up @@ -140,7 +149,7 @@ func (r *Runner) runBytes(envs map[string]string, dir, cmd string, args ...strin

name := nameArgs[0]

if len(nameArgs) > 2 {
if len(nameArgs) > 1 {
a := append([]string{}, nameArgs[1:]...)
a = append(a, args...)

Expand All @@ -154,10 +163,10 @@ func (r *Runner) runBytes(envs map[string]string, dir, cmd string, args ...strin
wrappedErr := fmt.Errorf(`%w

COMMAND:
%s
%s

OUTPUT:
%s`,
%s`,
Comment on lines 163 to +169
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format string adds a leading space before %s while indent(..., " ") already prefixes each line. This results in double-indentation (extra whitespace) in the wrapped error output. Consider removing the extra leading space in the format string (or adjusting indent usage) so the COMMAND/OUTPUT blocks are consistently indented.

Copilot uses AI. Check for mistakes.
err,
indent(c, " "),
indent(string(errBytes), " "),
Expand Down
91 changes: 91 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package chartify

import (
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)

func TestCreateFlagChain(t *testing.T) {
Expand Down Expand Up @@ -107,3 +110,91 @@ func TestFindSemVerInfo(t *testing.T) {
})
}
}

func TestKustomizeBin(t *testing.T) {
t.Run("KustomizeBinary option is set", func(t *testing.T) {
r := New(KustomizeBin("/custom/kustomize"))
got := r.kustomizeBin()
want := "/custom/kustomize"
if got != want {
t.Errorf("kustomizeBin() = %v, want %v", got, want)
}
})

t.Run("KUSTOMIZE_BIN environment variable", func(t *testing.T) {
if _, ok := os.LookupEnv("KUSTOMIZE_BIN"); ok {
t.Skip("KUSTOMIZE_BIN environment variable is already set")
}
os.Setenv("KUSTOMIZE_BIN", "/custom/kustomize")
defer os.Unsetenv("KUSTOMIZE_BIN")
r := New()
got := r.kustomizeBin()
Comment on lines +124 to +131
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mutates KUSTOMIZE_BIN but skips if it’s already set. In environments where KUSTOMIZE_BIN is set (e.g., developer shells), the later subtests that rely on PATH-based lookup will still see the env var and can fail unexpectedly. Prefer saving the original value, temporarily overriding/unsetting it for each subtest that needs PATH lookup, then restoring it in a defer instead of skipping.

Copilot uses AI. Check for mistakes.
want := "/custom/kustomize"
if got != want {
t.Errorf("kustomizeBin() = %v, want %v", got, want)
}
})

t.Run("fallback to kubectl kustomize when kustomize not found", func(t *testing.T) {
tmpDir := t.TempDir()
binDir := filepath.Join(tmpDir, "bin")
require.NoError(t, os.MkdirAll(binDir, 0755))

kubectlPath := filepath.Join(binDir, "kubectl")
kubectlContent := []byte("#!/bin/sh\necho 'kubectl version'\n")
require.NoError(t, os.WriteFile(kubectlPath, kubectlContent, 0755))

origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
os.Setenv("PATH", binDir)

r := New()
got := r.kustomizeBin()
want := "kubectl kustomize"
if got != want {
t.Errorf("kustomizeBin() = %v, want %v", got, want)
}
})

t.Run("use kustomize when both kustomize and kubectl exist in PATH", func(t *testing.T) {
tmpDir := t.TempDir()
binDir := filepath.Join(tmpDir, "bin")
require.NoError(t, os.MkdirAll(binDir, 0755))

kustomizePath := filepath.Join(binDir, "kustomize")
kustomizeContent := []byte("#!/bin/sh\necho 'kustomize version'\n")
require.NoError(t, os.WriteFile(kustomizePath, kustomizeContent, 0755))

kubectlPath := filepath.Join(binDir, "kubectl")
kubectlContent := []byte("#!/bin/sh\necho 'kubectl version'\n")
require.NoError(t, os.WriteFile(kubectlPath, kubectlContent, 0755))

origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
os.Setenv("PATH", binDir)

r := New()
got := r.kustomizeBin()
want := "kustomize"
if got != want {
t.Errorf("kustomizeBin() = %v, want %v", got, want)
}
})

t.Run("return kustomize as fallback when neither kustomize nor kubectl exist", func(t *testing.T) {
tmpDir := t.TempDir()
binDir := filepath.Join(tmpDir, "bin")
require.NoError(t, os.MkdirAll(binDir, 0755))

origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
os.Setenv("PATH", binDir)

r := New()
got := r.kustomizeBin()
want := "kustomize"
if got != want {
t.Errorf("kustomizeBin() = %v, want %v", got, want)
}
})
}
Loading