Skip to content
Draft
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
31 changes: 31 additions & 0 deletions chartify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,34 @@
})
}
}

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"))

Check failure on line 174 in chartify_test.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)

Check failure on line 174 in chartify_test.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)
// 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)
}
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
116 changes: 116 additions & 0 deletions kubectl_fallback_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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
`

Check failure on line 54 in kubectl_fallback_integration_test.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)

Check failure on line 54 in kubectl_fallback_integration_test.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)
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, "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")
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, " "))
})
}
94 changes: 59 additions & 35 deletions kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
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"`
Expand Down Expand Up @@ -56,6 +66,28 @@
SetKustomizeBuildOption(opts *KustomizeBuildOpts) error
}

// generateKustomizationFile creates a complete kustomization.yaml content
func (r *Runner) generateKustomizationFile(relPath string, opts KustomizeOpts) ([]byte, error) {
kustomization := KustomizationFile{
Resources: []string{relPath}, // Use resources instead of deprecated bases
}

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{}
Expand Down Expand Up @@ -103,42 +135,18 @@
if err != nil {
return "", err
}
baseFile := []byte("bases:\n- " + relPath + "\n")

Check failure on line 138 in kustomize.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)

Check failure on line 138 in kustomize.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)
// 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"}

Expand All @@ -159,7 +167,13 @@
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
}
Expand All @@ -173,7 +187,13 @@
}

// 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
Expand All @@ -193,12 +213,14 @@
// 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
Expand All @@ -208,12 +230,14 @@
// 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
Expand Down
38 changes: 38 additions & 0 deletions runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,44 @@
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) {

Check failure on line 114 in runner.go

View workflow job for this annotation

GitHub Actions / Lint

(*Runner).kustomizeBuildCommand - result 2 (error) is always nil (unparam)

Check failure on line 114 in runner.go

View workflow job for this annotation

GitHub Actions / Lint

(*Runner).kustomizeBuildCommand - result 2 (error) is always nil (unparam)
// 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

Check failure on line 124 in runner.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)

Check failure on line 124 in runner.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gci)
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...)

Expand Down
Loading