diff --git a/kubectl_kustomize_test.go b/kubectl_kustomize_test.go
new file mode 100644
index 0000000..fb16b84
--- /dev/null
+++ b/kubectl_kustomize_test.go
@@ -0,0 +1,164 @@
+package chartify
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// stubKubectlScript is a minimal sh script that acts as a kubectl stub for tests.
+// It handles `kubectl kustomize
[-o|-o FILE|--output FILE]` by writing
+// a minimal valid Kubernetes Deployment YAML to the specified output file.
+const stubKubectlScript = `#!/bin/sh
+if [ "$1" = "kustomize" ]; then
+ shift
+ OUTPUT=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ -o) OUTPUT="$2"; shift 2;;
+ --output) OUTPUT="$2"; shift 2;;
+ *) shift;;
+ esac
+ done
+ if [ -n "$OUTPUT" ]; then
+ printf 'apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: stub\n' > "$OUTPUT"
+ fi
+fi
+`
+
+// writeStubKubectl creates a stub kubectl script in dir.
+func writeStubKubectl(t *testing.T, dir string) {
+ t.Helper()
+ p := filepath.Join(dir, "kubectl")
+ require.NoError(t, os.WriteFile(p, []byte(stubKubectlScript), 0755))
+}
+
+// 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) {
+ // Create a stub kubectl so the test is self-contained and always runs in CI.
+ stubDir := t.TempDir()
+ writeStubKubectl(t, stubDir)
+
+ origPath := os.Getenv("PATH")
+ defer os.Setenv("PATH", origPath)
+ os.Setenv("PATH", stubDir+string(os.PathListSeparator)+origPath)
+
+ 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("Patch succeeds with kubectl kustomize option", func(t *testing.T) {
+ // Create a stub kubectl so the test is self-contained and always runs in CI.
+ stubDir := t.TempDir()
+ writeStubKubectl(t, stubDir)
+
+ origPath := os.Getenv("PATH")
+ defer os.Setenv("PATH", origPath)
+ os.Setenv("PATH", stubDir+string(os.PathListSeparator)+origPath)
+
+ tempDir := t.TempDir()
+
+ // Write a minimal manifest file that Patch() will reference.
+ manifestPath := filepath.Join(tempDir, "templates", "deploy.yaml")
+ require.NoError(t, os.MkdirAll(filepath.Dir(manifestPath), 0755))
+ require.NoError(t, os.WriteFile(manifestPath, []byte(`apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: test
+`), 0644))
+
+ r := New(KustomizeBin("kubectl kustomize"))
+ err := r.Patch(tempDir, []string{manifestPath}, &PatchOpts{})
+ require.NoError(t, err)
+ })
+
+ 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 Chartify values files or kustomize build options is not supported when using 'kubectl kustomize'")
+ })
+}
diff --git a/kustomize.go b/kustomize.go
index f81f3fd..5a92256 100644
--- a/kustomize.go
+++ b/kustomize.go
@@ -84,6 +84,10 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize
panic("--set is not yet supported for kustomize-based apps! Use -f/--values flag instead.")
}
+ // Resolve the kustomize binary once so PATH lookups are not repeated for every check.
+ bin := r.kustomizeBin()
+ usingKubectl := bin == "kubectl kustomize"
+
prevDir, err := os.Getwd()
if err != nil {
return "", err
@@ -110,46 +114,62 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize
}
if len(kustomizeOpts.Images) > 0 {
+ if usingKubectl {
+ return "", fmt.Errorf("setting images via Chartify values files or kustomize build options 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())
}
- _, err := r.runInDir(tempDir, r.kustomizeBin(), args...)
+ _, err := r.runInDir(tempDir, bin, args...)
if err != nil {
return "", err
}
}
if kustomizeOpts.NamePrefix != "" {
- _, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "nameprefix", kustomizeOpts.NamePrefix)
+ if usingKubectl {
+ return "", fmt.Errorf("setting namePrefix via Chartify values files or kustomize build options is not supported when using 'kubectl kustomize'. Please set namePrefix directly in your kustomization.yaml file")
+ }
+ _, err := r.runInDir(tempDir, bin, "edit", "set", "nameprefix", kustomizeOpts.NamePrefix)
if err != nil {
fmt.Println(err)
return "", err
}
}
if kustomizeOpts.NameSuffix != "" {
+ if usingKubectl {
+ return "", fmt.Errorf("setting nameSuffix via Chartify values files or kustomize build options 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)
+ _, err := r.runInDir(tempDir, bin, "edit", "set", "namesuffix", "--", kustomizeOpts.NameSuffix)
if err != nil {
return "", err
}
}
if kustomizeOpts.Namespace != "" {
- _, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "namespace", kustomizeOpts.Namespace)
+ if usingKubectl {
+ return "", fmt.Errorf("setting namespace via Chartify values files or kustomize build options is not supported when using 'kubectl kustomize'. Please set namespace directly in your kustomization.yaml file")
+ }
+ _, err := r.runInDir(tempDir, bin, "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 !usingKubectl {
+ kustomizeArgs = append(kustomizeArgs, "build")
+ }
if u.EnableAlphaPlugins {
- f, err := r.kustomizeEnableAlphaPluginsFlag()
+ f, err := r.kustomizeEnableAlphaPluginsFlag(usingKubectl)
if err != nil {
return "", err
}
kustomizeArgs = append(kustomizeArgs, f)
}
- f, err := r.kustomizeLoadRestrictionsNoneFlag()
+ f, err := r.kustomizeLoadRestrictionsNoneFlag(usingKubectl)
if err != nil {
return "", err
}
@@ -159,7 +179,7 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize
kustomizeArgs = append(kustomizeArgs, "--helm-command="+u.HelmBinary)
}
- out, err := r.runInDir(tempDir, r.kustomizeBin(), append(kustomizeArgs, tempDir)...)
+ out, err := r.runInDir(tempDir, bin, append(kustomizeArgs, tempDir)...)
if err != nil {
return "", err
}
@@ -193,7 +213,10 @@ func (r *Runner) kustomizeVersion() (*semver.Version, error) {
// 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) {
+func (r *Runner) kustomizeEnableAlphaPluginsFlag(usingKubectl bool) (string, error) {
+ if usingKubectl {
+ return "--enable-alpha-plugins", nil
+ }
version, err := r.kustomizeVersion()
if err != nil {
return "", err
@@ -208,7 +231,10 @@ func (r *Runner) kustomizeEnableAlphaPluginsFlag() (string, error) {
// the root argument.
// Above Kustomize v3, it is `--load-restrictor=LoadRestrictionsNone`.
// Below Kustomize v3 (including v3), it is `--load_restrictor=none`.
-func (r *Runner) kustomizeLoadRestrictionsNoneFlag() (string, error) {
+func (r *Runner) kustomizeLoadRestrictionsNoneFlag(usingKubectl bool) (string, error) {
+ if usingKubectl {
+ return "--load-restrictor=LoadRestrictionsNone", nil
+ }
version, err := r.kustomizeVersion()
if err != nil {
return "", err
diff --git a/patch.go b/patch.go
index e41c2eb..0dbebdf 100644
--- a/patch.go
+++ b/patch.go
@@ -43,6 +43,10 @@ func (r *Runner) Patch(tempDir string, generatedManifestFiles []string, opts ...
}
}
+ // Resolve the kustomize binary once so PATH lookups are not repeated for every check.
+ bin := r.kustomizeBin()
+ usingKubectl := bin == "kubectl kustomize"
+
r.Logf("patching files: %v", generatedManifestFiles)
// Detect if CRDs originally came from templates/ directory
@@ -181,19 +185,26 @@ resources:
renderedFileName := "all.patched.yaml"
renderedFile := filepath.Join(tempDir, renderedFileName)
- r.Logf("Generating %s", renderedFile)
+ r.Logf("Generating %s", renderedFileName)
+
+ kustomizeArgs := []string{"--output", renderedFile}
- kustomizeArgs := []string{"build", tempDir, "--output", renderedFile}
+ if !usingKubectl {
+ kustomizeArgs = append([]string{"build", tempDir}, kustomizeArgs...)
+ } else {
+ // kubectl kustomize does not use the "build" subcommand; pass tempDir as the target directly.
+ kustomizeArgs = append([]string{tempDir}, kustomizeArgs...)
+ }
if u.EnableAlphaPlugins {
- f, err := r.kustomizeEnableAlphaPluginsFlag()
+ f, err := r.kustomizeEnableAlphaPluginsFlag(usingKubectl)
if err != nil {
return err
}
kustomizeArgs = append(kustomizeArgs, f)
}
- _, err := r.run(nil, r.kustomizeBin(), kustomizeArgs...)
+ _, err := r.run(nil, bin, kustomizeArgs...)
if err != nil {
return err
}
diff --git a/runner.go b/runner.go
index 61eb0ca..8bf0827 100644
--- a/runner.go
+++ b/runner.go
@@ -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"
}
@@ -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...)
diff --git a/util_test.go b/util_test.go
index 6a944c4..92550fa 100644
--- a/util_test.go
+++ b/util_test.go
@@ -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) {
@@ -107,3 +110,122 @@ 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) {
+ origVal, hadVal := os.LookupEnv("KUSTOMIZE_BIN")
+ defer func() {
+ if hadVal {
+ os.Setenv("KUSTOMIZE_BIN", origVal)
+ } else {
+ os.Unsetenv("KUSTOMIZE_BIN")
+ }
+ }()
+ os.Setenv("KUSTOMIZE_BIN", "/custom/kustomize")
+ r := New()
+ got := r.kustomizeBin()
+ 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)
+
+ // Ensure KUSTOMIZE_BIN does not override PATH-based lookup.
+ origKustomizeBin, hadKustomizeBin := os.LookupEnv("KUSTOMIZE_BIN")
+ defer func() {
+ if hadKustomizeBin {
+ os.Setenv("KUSTOMIZE_BIN", origKustomizeBin)
+ }
+ }()
+ os.Unsetenv("KUSTOMIZE_BIN")
+
+ 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)
+
+ // Ensure KUSTOMIZE_BIN does not override PATH-based lookup.
+ origKustomizeBin, hadKustomizeBin := os.LookupEnv("KUSTOMIZE_BIN")
+ defer func() {
+ if hadKustomizeBin {
+ os.Setenv("KUSTOMIZE_BIN", origKustomizeBin)
+ }
+ }()
+ os.Unsetenv("KUSTOMIZE_BIN")
+
+ 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)
+
+ // Ensure KUSTOMIZE_BIN does not override PATH-based lookup.
+ origKustomizeBin, hadKustomizeBin := os.LookupEnv("KUSTOMIZE_BIN")
+ defer func() {
+ if hadKustomizeBin {
+ os.Setenv("KUSTOMIZE_BIN", origKustomizeBin)
+ }
+ }()
+ os.Unsetenv("KUSTOMIZE_BIN")
+
+ r := New()
+ got := r.kustomizeBin()
+ want := "kustomize"
+ if got != want {
+ t.Errorf("kustomizeBin() = %v, want %v", got, want)
+ }
+ })
+}