feat: Allow using kubectl built-in kustomize when separate kustomize binary is missing#173
feat: Allow using kubectl built-in kustomize when separate kustomize binary is missing#173
Conversation
…binary is missing Implements #63: Automatically fallback to kubectl's built-in kustomize when the standalone kustomize binary is not available. Changes: - Modified kustomizeBin() to detect and use kubectl kustomize as fallback - Fixed runBytes() to properly handle commands with subcommands like 'kubectl kustomize' - Added isUsingKubectlKustomize() helper to detect when using kubectl kustomize - Updated kustomizeEnableAlphaPluginsFlag() and kustomizeLoadRestrictionsNoneFlag() to skip version detection when using kubectl kustomize - Added clear error messages for unsupported features (edit subcommands) when using kubectl kustomize - Modified KustomizeBuild() to omit 'build' argument when using kubectl kustomize - Modified patch.go to omit 'build' argument and tempDir when using kubectl kustomize - Added TestKustomizeBin test to verify fallback behavior - Added TestKubectlKustomizeFallback integration test Signed-off-by: yxxhero <aiopsclub@163.com>
There was a problem hiding this comment.
Pull request overview
Adds an automatic fallback to kubectl’s built-in kustomize implementation when a standalone kustomize binary is not available, and updates command execution to support multi-word “commands” like kubectl kustomize.
Changes:
- Update
Runner.kustomizeBin()to preferkustomizeif present, otherwise fall back tokubectl kustomize. - Fix
Runner.runBytes()to correctly handle commands with subcommands embedded in thecmdstring. - Adjust kustomize build/patch flows and add tests around the new behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| util_test.go | Adds unit tests for kustomizeBin() selection logic (option, PATH lookup, fallback). |
| runner.go | Implements the fallback selection in kustomizeBin() and improves command parsing for multi-word commands. |
| patch.go | Updates patch rendering to support kubectl-kustomize mode (argument handling changes). |
| kustomize.go | Adds kubectl-kustomize detection, skips version probing, and adjusts build/edit behavior accordingly. |
| kubectl_kustomize_test.go | Adds a test intended to validate kubectl kustomize support. |
Comments suppressed due to low confidence (1)
patch.go:201
- When using
kubectl kustomize, this invocation omits the kustomization directory argument and also runs withdir==""(current working directory). That meanskubectl kustomizewill not read the generated kustomization.yaml undertempDir, so patch rendering will fail. PasstempDiras the final arg for the kubectl case (similar toKustomizeBuild), or switch torunInDir(tempDir, ...)and rely on the default.target.
kustomizeArgs := []string{"--output", renderedFile}
if !r.isUsingKubectlKustomize() {
kustomizeArgs = append([]string{"build", tempDir}, kustomizeArgs...)
}
if u.EnableAlphaPlugins {
f, err := r.kustomizeEnableAlphaPluginsFlag()
if err != nil {
return err
}
kustomizeArgs = append(kustomizeArgs, f)
}
_, err := r.run(nil, r.kustomizeBin(), kustomizeArgs...)
if err != nil {
util_test.go
Outdated
| t.Run("KUSTOMIZE_BIN environment variable", func(t *testing.T) { | ||
| if _, err := exec.LookPath("kustomize"); err != nil { | ||
| t.Skip("kustomize binary not found in PATH") | ||
| } | ||
| 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(KustomizeBin(os.Getenv("KUSTOMIZE_BIN"))) | ||
| got := r.kustomizeBin() | ||
| want := "/custom/kustomize" | ||
| if got != want { | ||
| t.Errorf("kustomizeBin() = %v, want %v", got, want) | ||
| } | ||
| }) |
There was a problem hiding this comment.
This subtest name suggests that kustomizeBin() reads KUSTOMIZE_BIN directly, but the test actually passes the env var value via KustomizeBin(...) (same behavior as the first subtest). Rename the subtest to reflect that it’s testing the option value, or update kustomizeBin() to consult KUSTOMIZE_BIN if that’s intended.
util_test.go
Outdated
| t.Run("kustomize binary exists in PATH", func(t *testing.T) { | ||
| if _, err := exec.LookPath("kustomize"); err != nil { | ||
| t.Skip("kustomize binary not found in PATH") | ||
| } | ||
| r := New() | ||
| got := r.kustomizeBin() | ||
| want := "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) { | ||
| if _, err := exec.LookPath("kubectl"); err != nil { | ||
| t.Skip("kubectl binary not found in PATH") | ||
| } | ||
| if _, err := exec.LookPath("kustomize"); err == nil { | ||
| t.Skip("kustomize binary found, cannot test fallback") | ||
| } | ||
| r := New() | ||
| got := r.kustomizeBin() | ||
| want := "kubectl kustomize" | ||
| if got != want { | ||
| t.Errorf("kustomizeBin() = %v, want %v", got, want) | ||
| } | ||
| }) |
There was a problem hiding this comment.
These PATH-dependent branches are mostly skipped depending on what binaries happen to be installed (e.g., the fallback case is skipped whenever kustomize exists), so the new fallback logic may not be exercised in CI. Consider making the test deterministic by setting PATH to a temp dir containing a fake kubectl executable (and no kustomize) so exec.LookPath reliably drives the intended branch.
| func TestKubectlKustomizeFallback(t *testing.T) { | ||
| if _, err := exec.LookPath("kubectl"); err != nil { | ||
| t.Skip("kubectl binary not found in PATH") | ||
| } | ||
|
|
||
| t.Run("KustomizeBuild 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") | ||
| 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) | ||
| }) |
There was a problem hiding this comment.
This test is named as a "fallback" test but it forces KustomizeBin("kubectl kustomize") and skips when kubectl isn’t installed, so it doesn’t verify the automatic fallback behavior in kustomizeBin() and may be skipped in CI. Consider making it self-contained by creating a fake kubectl on PATH (and no kustomize) and asserting New().kustomizeBin() selects it, or rename the test to reflect what it actually validates.
Address review comments: - Add KUSTOMIZE_BIN environment variable support to kustomizeBin() for consistency with helmBin() - Make TestKustomizeBin tests deterministic by using fake executables and controlled PATH instead of relying on system binaries - Make TestKubectlKustomizeFallback test self-contained - Add test for edit commands not supported with kubectl kustomize Signed-off-by: yxxhero <aiopsclub@163.com>
Implements #63: Automatically fallback to kubectl's built-in kustomize when the standalone kustomize binary is not available.
Changes: