From a2c20ed0f2995b58b22689bde9ad9d2c120322ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:33:16 +0000 Subject: [PATCH 1/4] Initial plan From 2568d5f3eefc583eaee9419ccfb1b5a87ab62f38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:38:21 +0000 Subject: [PATCH 2/4] Initial plan for kubectl kustomize fallback support Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com> --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index b99da3a..be1e1ed 100644 --- a/go.sum +++ b/go.sum @@ -343,8 +343,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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= -helm.sh/helm/v3 v3.18.5 h1:Cc3Z5vd6kDrZq9wO9KxKLNEickiTho6/H/dBNRVSos4= -helm.sh/helm/v3 v3.18.5/go.mod h1:L/dXDR2r539oPlFP1PJqKAC1CUgqHJDLkxKpDGrWnyg= helm.sh/helm/v3 v3.18.6 h1:S/2CqcYnNfLckkHLI0VgQbxgcDaU3N4A/46E3n9wSNY= helm.sh/helm/v3 v3.18.6/go.mod h1:L/dXDR2r539oPlFP1PJqKAC1CUgqHJDLkxKpDGrWnyg= k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= From d53f0045a98c74d0294599b22329ec62e7abb7c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:43:13 +0000 Subject: [PATCH 3/4] Implement kubectl kustomize fallback functionality Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com> --- chartify_test.go | 31 ++++++++ kubectl_fallback_integration_test.go | 115 +++++++++++++++++++++++++++ kustomize.go | 94 ++++++++++++++-------- runner.go | 38 +++++++++ 4 files changed, 243 insertions(+), 35 deletions(-) create mode 100644 kubectl_fallback_integration_test.go diff --git a/chartify_test.go b/chartify_test.go index 4516449..141e07b 100644 --- a/chartify_test.go +++ b/chartify_test.go @@ -166,3 +166,34 @@ func TestUseHelmChartsInKustomize(t *testing.T) { }) } } + +func TestKubectlKustomizeFallback(t *testing.T) { + // Test the fallback functionality when kustomize binary is not available + // Use a non-existent binary name to simulate missing kustomize + r := New(UseHelm3(true), KustomizeBin("non-existent-kustomize-binary")) + + // Test that isKustomizeBinaryAvailable returns false for non-existent binary + available := r.isKustomizeBinaryAvailable() + require.False(t, available, "non-existent binary should not be available") + + // Test the kustomizeBuildCommand function returns kubectl command for fallback + buildArgs := []string{"-o", "/tmp/output.yaml", "build", "--enable-helm"} + targetDir := "/tmp/testdir" + + cmd, args, err := r.kustomizeBuildCommand(buildArgs, targetDir) + require.NoError(t, err) + require.Equal(t, "kubectl", cmd) + require.Contains(t, args, "kustomize") + require.Contains(t, args, targetDir) + require.Contains(t, args, "--enable-helm") + + // Test with a real kustomize binary (should not fallback) + r2 := New(UseHelm3(true), KustomizeBin("kustomize")) + available2 := r2.isKustomizeBinaryAvailable() + require.True(t, available2, "kustomize binary should be available") + + cmd2, args2, err2 := r2.kustomizeBuildCommand(buildArgs, targetDir) + require.NoError(t, err2) + require.Equal(t, "kustomize", cmd2) + require.Equal(t, append(buildArgs, targetDir), args2) +} diff --git a/kubectl_fallback_integration_test.go b/kubectl_fallback_integration_test.go new file mode 100644 index 0000000..b0cfba3 --- /dev/null +++ b/kubectl_fallback_integration_test.go @@ -0,0 +1,115 @@ +package chartify + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestKubectlKustomizeFallbackIntegration(t *testing.T) { + // Create a temporary directory for our test + tempDir, err := os.MkdirTemp("", "kubectl-fallback-integration") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // Create a simple kubernetes manifest + manifestContent := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: app + image: nginx:1.20 + ports: + - containerPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: test-app-service + namespace: default +spec: + selector: + app: test-app + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: ClusterIP +` + + manifestPath := filepath.Join(tempDir, "manifest.yaml") + err = os.WriteFile(manifestPath, []byte(manifestContent), 0644) + require.NoError(t, err) + + // Test with non-existent kustomize binary (should use kubectl fallback) + t.Run("kubectl fallback scenario", func(t *testing.T) { + runner := New(KustomizeBin("non-existent-kustomize")) + + // Test kustomize build with image replacement + kustomizeOpts := KustomizeOpts{ + Images: []KustomizeImage{ + {Name: "nginx", NewTag: "1.21"}, + }, + NamePrefix: "prefix-", + Namespace: "test-namespace", + } + + // Generate kustomization content + relPath := "." + kustomizationContent, err := runner.generateKustomizationFile(relPath, kustomizeOpts) + require.NoError(t, err) + + // Verify the generated kustomization content + kustomizationStr := string(kustomizationContent) + require.Contains(t, kustomizationStr, "bases:") + require.Contains(t, kustomizationStr, "images:") + require.Contains(t, kustomizationStr, "namePrefix: prefix-") + require.Contains(t, kustomizationStr, "namespace: test-namespace") + require.Contains(t, kustomizationStr, "nginx") + require.Contains(t, kustomizationStr, "1.21") + + // Write kustomization.yaml + kustomizationPath := filepath.Join(tempDir, "kustomization.yaml") + err = os.WriteFile(kustomizationPath, kustomizationContent, 0644) + require.NoError(t, err) + + // Test the build command generation for kubectl fallback + buildArgs := []string{"-o", "/tmp/output.yaml", "build", "--enable-helm"} + cmd, args, err := runner.kustomizeBuildCommand(buildArgs, tempDir) + require.NoError(t, err) + require.Equal(t, "kubectl", cmd) + require.Contains(t, args, "kustomize") + require.Contains(t, args, tempDir) + require.Contains(t, args, "--enable-helm") + + t.Logf("kubectl fallback command: %s %s", cmd, strings.Join(args, " ")) + }) + + // Test with real kustomize binary (should use kustomize directly) + t.Run("normal kustomize scenario", func(t *testing.T) { + runner := New(KustomizeBin("kustomize")) + + buildArgs := []string{"-o", "/tmp/output.yaml", "build", "--enable-helm"} + cmd, args, err := runner.kustomizeBuildCommand(buildArgs, tempDir) + require.NoError(t, err) + require.Equal(t, "kustomize", cmd) + require.Equal(t, append(buildArgs, tempDir), args) + + t.Logf("normal kustomize command: %s %s", cmd, strings.Join(args, " ")) + }) +} \ No newline at end of file diff --git a/kustomize.go b/kustomize.go index f81f3fd..6e13d9d 100644 --- a/kustomize.go +++ b/kustomize.go @@ -17,6 +17,16 @@ type KustomizeOpts struct { Namespace string `yaml:"namespace"` } +// KustomizationFile represents the structure of a kustomization.yaml file +type KustomizationFile struct { + Resources []string `yaml:"resources,omitempty"` + Bases []string `yaml:"bases,omitempty"` + Images []KustomizeImage `yaml:"images,omitempty"` + NamePrefix string `yaml:"namePrefix,omitempty"` + NameSuffix string `yaml:"nameSuffix,omitempty"` + Namespace string `yaml:"namespace,omitempty"` +} + type KustomizeImage struct { Name string `yaml:"name"` NewName string `yaml:"newName"` @@ -56,6 +66,28 @@ type KustomizeBuildOption interface { SetKustomizeBuildOption(opts *KustomizeBuildOpts) error } +// generateKustomizationFile creates a complete kustomization.yaml content +func (r *Runner) generateKustomizationFile(relPath string, opts KustomizeOpts) ([]byte, error) { + kustomization := KustomizationFile{ + Bases: []string{relPath}, + } + + if len(opts.Images) > 0 { + kustomization.Images = opts.Images + } + if opts.NamePrefix != "" { + kustomization.NamePrefix = opts.NamePrefix + } + if opts.NameSuffix != "" { + kustomization.NameSuffix = opts.NameSuffix + } + if opts.Namespace != "" { + kustomization.Namespace = opts.Namespace + } + + return yaml.Marshal(&kustomization) +} + func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...KustomizeBuildOption) (string, error) { kustomizeOpts := KustomizeOpts{} u := &KustomizeBuildOpts{} @@ -103,42 +135,18 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize if err != nil { return "", err } - baseFile := []byte("bases:\n- " + relPath + "\n") + + // Generate complete kustomization.yaml file directly instead of using edit commands + kustomizationContent, err := r.generateKustomizationFile(relPath, kustomizeOpts) + if err != nil { + return "", fmt.Errorf("generating kustomization.yaml: %v", err) + } + kustomizationPath := path.Join(tempDir, "kustomization.yaml") - if err := r.WriteFile(kustomizationPath, baseFile, 0644); err != nil { + if err := r.WriteFile(kustomizationPath, kustomizationContent, 0644); err != nil { return "", err } - if len(kustomizeOpts.Images) > 0 { - args := []string{"edit", "set", "image"} - for _, image := range kustomizeOpts.Images { - args = append(args, image.String()) - } - _, err := r.runInDir(tempDir, r.kustomizeBin(), args...) - if err != nil { - return "", err - } - } - if kustomizeOpts.NamePrefix != "" { - _, err := r.runInDir(tempDir, r.kustomizeBin(), "edit", "set", "nameprefix", kustomizeOpts.NamePrefix) - if err != nil { - fmt.Println(err) - return "", err - } - } - if kustomizeOpts.NameSuffix != "" { - // "--" 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 != "" { - _, 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"} @@ -159,7 +167,13 @@ 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)...) + // Use kubectl kustomize fallback if standalone kustomize is not available + buildCmd, buildArgs, err := r.kustomizeBuildCommand(kustomizeArgs, tempDir) + if err != nil { + return "", err + } + + out, err := r.runInDir(tempDir, buildCmd, buildArgs...) if err != nil { return "", err } @@ -173,7 +187,13 @@ func (r *Runner) KustomizeBuild(srcDir string, tempDir string, opts ...Kustomize } // kustomizeVersion returns the kustomize binary version. +// Returns nil if kustomize binary is not available (fallback scenario). func (r *Runner) kustomizeVersion() (*semver.Version, error) { + // Skip version detection if using a fallback scenario + if !r.isKustomizeBinaryAvailable() { + return nil, nil + } + versionInfo, err := r.run(nil, r.kustomizeBin(), "version") if err != nil { return nil, err @@ -193,12 +213,14 @@ 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`. +// Uses modern flag format when kustomize binary is not available (kubectl fallback). func (r *Runner) kustomizeEnableAlphaPluginsFlag() (string, error) { version, err := r.kustomizeVersion() if err != nil { return "", err } - if version.Major() > 3 { + // If version is nil (fallback scenario), use modern flag format + if version == nil || version.Major() > 3 { return "--enable-alpha-plugins", nil } return "--enable_alpha_plugins", nil @@ -208,12 +230,14 @@ 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`. +// Uses modern flag format when kustomize binary is not available (kubectl fallback). func (r *Runner) kustomizeLoadRestrictionsNoneFlag() (string, error) { version, err := r.kustomizeVersion() if err != nil { return "", err } - if version.Major() > 3 { + // If version is nil (fallback scenario), use modern flag format + if version == nil || version.Major() > 3 { return "--load-restrictor=LoadRestrictionsNone", nil } return "--load_restrictor=none", nil diff --git a/runner.go b/runner.go index e0d744d..9c5e9c1 100644 --- a/runner.go +++ b/runner.go @@ -103,6 +103,44 @@ func (r *Runner) kustomizeBin() string { return "kustomize" } +// isKustomizeBinaryAvailable checks if the kustomize binary is available +func (r *Runner) isKustomizeBinaryAvailable() bool { + _, _, err := r.captureBytes(r.kustomizeBin(), []string{"version"}, "", nil) + return err == nil +} + +// kustomizeBuildCommand returns the appropriate command and args for kustomize build operation +// Falls back to "kubectl kustomize" if standalone kustomize binary is not available +func (r *Runner) kustomizeBuildCommand(buildArgs []string, targetDir string) (string, []string, error) { + // First check if the configured kustomize binary is available + if r.isKustomizeBinaryAvailable() { + return r.kustomizeBin(), append(buildArgs, targetDir), nil + } + + // Fallback to kubectl kustomize + // kubectl kustomize requires different argument order: kubectl kustomize [flags] DIR + // We need to transform: kustomize [args] build [more-args] DIR + // Into: kubectl kustomize [more-args] DIR + + kubectlArgs := []string{"kustomize"} + + // Extract build-specific flags from buildArgs (everything except "build") + for i, arg := range buildArgs { + if arg == "build" { + // Add everything after "build" except the target dir (which gets added at the end) + kubectlArgs = append(kubectlArgs, buildArgs[i+1:]...) + break + } + // Add everything before "build" + kubectlArgs = append(kubectlArgs, arg) + } + + // Add target directory at the end + kubectlArgs = append(kubectlArgs, targetDir) + + return "kubectl", kubectlArgs, nil +} + func (r *Runner) run(envs map[string]string, cmd string, args ...string) (string, error) { bytes, err := r.runBytes(envs, "", cmd, args...) From 75aa055f90de5d356182d280d7ffb3c35250bdd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 23:46:54 +0000 Subject: [PATCH 4/4] Update kustomization generation to use resources instead of deprecated bases Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com> --- kubectl_fallback_integration_test.go | 3 ++- kustomize.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/kubectl_fallback_integration_test.go b/kubectl_fallback_integration_test.go index b0cfba3..7fd0b51 100644 --- a/kubectl_fallback_integration_test.go +++ b/kubectl_fallback_integration_test.go @@ -76,7 +76,8 @@ spec: // Verify the generated kustomization content kustomizationStr := string(kustomizationContent) - require.Contains(t, kustomizationStr, "bases:") + require.Contains(t, kustomizationStr, "resources:") + require.NotContains(t, kustomizationStr, "bases:") // Should not use deprecated bases require.Contains(t, kustomizationStr, "images:") require.Contains(t, kustomizationStr, "namePrefix: prefix-") require.Contains(t, kustomizationStr, "namespace: test-namespace") diff --git a/kustomize.go b/kustomize.go index 6e13d9d..3271b26 100644 --- a/kustomize.go +++ b/kustomize.go @@ -69,7 +69,7 @@ type KustomizeBuildOption interface { // generateKustomizationFile creates a complete kustomization.yaml content func (r *Runner) generateKustomizationFile(relPath string, opts KustomizeOpts) ([]byte, error) { kustomization := KustomizationFile{ - Bases: []string{relPath}, + Resources: []string{relPath}, // Use resources instead of deprecated bases } if len(opts.Images) > 0 {