diff --git a/README.md b/README.md index a9b0f97..d0c6930 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ kustomize-build-dirs takes a list of filenames, and for each one walks up the directory tree until it finds a directory containing `kustomization.yaml` then runs `kustomize build` on that directory, saving the output in the directory given by `--out-dir`. +It also truncates secrets, so that we don't need to decrypt them in order to check +if manifests are correct. This program should only be run from the root of a Git repository. diff --git a/cmd/kustomize-build-dirs/main.go b/cmd/kustomize-build-dirs/main.go index 97dd1cc..b3fc5a7 100644 --- a/cmd/kustomize-build-dirs/main.go +++ b/cmd/kustomize-build-dirs/main.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -11,10 +12,17 @@ import ( "github.com/urfave/cli/v2" "golang.org/x/sync/errgroup" + "gopkg.in/yaml.v2" ) const manifestFileName = "manifests.yaml" +// Kustomization represents the structure of a Kustomization file +type Kustomization struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` +} + // variable used for testing var getwdFunc = os.Getwd @@ -66,6 +74,11 @@ func kustomizeBuildDirs(outDir string, doTruncateSecrets bool, filepaths []strin return err } + kustomizationRoots, err = removeComponentKustomizations(rootDir, kustomizationRoots) + if err != nil { //go-cov:skip + return err + } + // truncate secrets so we can run `kustomize build` without having to decrypt them if doTruncateSecrets { if err := truncateSecrets(rootDir, kustomizationRoots); err != nil { @@ -111,6 +124,7 @@ func findKustomizationRoots(root string, paths []string) ([]string, error) { if kustomizationRoot == "" { continue } + if _, exists := rootsMap[kustomizationRoot]; !exists { fmt.Printf("Found kustomization build dir: %s\n", kustomizationRoot) rootsMap[kustomizationRoot] = struct{}{} @@ -131,8 +145,8 @@ func findKustomizationRoot(repoRoot string, relativePath string) (string, error) case err == nil: // found 'kustomization.yaml' return dir, nil - case err != nil && !os.IsNotExist(err): - return "", fmt.Errorf("Error checking for file in %s: %v", dir, err) + case !os.IsNotExist(err): + return "", fmt.Errorf("error checking for file in %s: %v", dir, err) default: // file not found, continue up the directory tree continue @@ -141,6 +155,47 @@ func findKustomizationRoot(repoRoot string, relativePath string) (string, error) return "", nil } +// removeComponentKustomizations checks the list of the kustomization files, and removes those with +// kind: Component. +// We can't expect standalone Component kustomization files to correctly render. +func removeComponentKustomizations(kustomizationRoot string, paths []string) ([]string, error) { + pathsNoComponent := []string{} + for _, path := range paths { + isComponent, err := checkIfIsComponent( + filepath.Join(kustomizationRoot, path, "kustomization.yaml"), + ) + if err != nil { //go-cov:skip + return nil, err + } + if !isComponent { + pathsNoComponent = append(pathsNoComponent, path) + } + } + return pathsNoComponent, nil +} + +func checkIfIsComponent(filepath string) (bool, error) { + file, err := os.Open(filepath) + if err != nil { //go-cov:skip + return false, fmt.Errorf("failed opening kustomization file: %s: %v", filepath, err) + } + defer file.Close() + + // Read the file's content + data, err := io.ReadAll(file) + if err != nil { //go-cov:skip + return false, fmt.Errorf("error reading file: %v", err) + } + + // Unmarshal the YAML into the struct + var kustomization Kustomization + err = yaml.Unmarshal(data, &kustomization) + if err != nil { //go-cov:skip + return false, fmt.Errorf("error unmarshaling YAML: %v", err) + } + return kustomization.Kind == "Component", nil +} + func truncateSecrets(rootDir string, dirs []string) error { secrets, err := findSecrets(rootDir, dirs) if err != nil { diff --git a/cmd/kustomize-build-dirs/main_test.go b/cmd/kustomize-build-dirs/main_test.go index e7b33a4..eb6b729 100644 --- a/cmd/kustomize-build-dirs/main_test.go +++ b/cmd/kustomize-build-dirs/main_test.go @@ -22,6 +22,13 @@ kind: Kustomization resources: - deployment.yaml ` + + componentKustomization = `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Component + +patches: + - path: deployment.yaml +` simpleDeploymentTemplate = `apiVersion: apps/v1 kind: Deployment metadata: @@ -44,7 +51,12 @@ func requireErorrPrefix(t *testing.T, err error, prefix string) { t.Helper() require.Error(t, err) - require.LessOrEqual(t, len(prefix), len(err.Error()), "error cannot be shorter than prefix") + require.LessOrEqual( + t, + len(prefix), + len(err.Error()), + fmt.Sprintf("error cannot be shorter than prefix, err: %s, prefix: %s", err, prefix), + ) require.Equalf(t, prefix, err.Error()[:len(prefix)], "full error: %v", err) } @@ -111,7 +123,7 @@ func TestFailsWhenUnableToListSecrets(t *testing.T) { workDir, ) - // run command outside of any Git directory + // run command outside any Git directory err = kustomizeBuildDirs(mockoutDir, true, []string{"kustomization.yaml"}) requireErorrPrefix(t, err, expectedErrPrefix) } @@ -148,7 +160,7 @@ func TestFailsWhenUnableToFindKustomizations(t *testing.T) { require.NoError(t, os.Chmod(unredableDirPath, 0o600)) // restore permissions so we can cleanup defer os.Chmod(unredableDirPath, 0o700) //nolint:errcheck - expectedErrPrefix := "Error checking for file in manifests:" + expectedErrPrefix := "error checking for file in manifests:" err := kustomizeBuildDirs(mockoutDir, false, []string{"manifests/kustomization.yaml"}) requireErorrPrefix(t, err, expectedErrPrefix) @@ -305,6 +317,20 @@ func compareResults( } } +func TestDontRenderComponent(t *testing.T) { + gitDir, outDir := setupTest(t) + + manifestPath := filepath.Join("manifests", "deployment.yaml") + repoFiles := map[string]string{ + filepath.Join("manifests", "kustomization.yaml"): componentKustomization, + manifestPath: simpleDeployment, + } + buildGitRepo(t, gitDir, repoFiles) + + require.NoError(t, kustomizeBuildDirs(outDir, false, []string{manifestPath})) + require.NoFileExists(t, outDir) +} + func TestWriteSingleManifest(t *testing.T) { gitDir, outDir := setupTest(t) @@ -367,6 +393,31 @@ func TestWriteMultipleManifests(t *testing.T) { compareResults(t, outDir, expectedContents, readOutDir(t, outDir)) } +func TestWriteMultipleManifestsOneIscomponent(t *testing.T) { + gitDir, outDir := setupTest(t) + + firstDeploymentPath := filepath.Join("first-project", "deployment.yaml") + firstDeploymentcontent := fmt.Sprintf(simpleDeploymentTemplate, "first-app") + secondDeploymentPath := filepath.Join("second-project", "deployment.yaml") + secondDeploymentcontent := fmt.Sprintf(simpleDeploymentTemplate, "second-app") + repoFiles := map[string]string{ + firstDeploymentPath: firstDeploymentcontent, + filepath.Join("first-project", "kustomization.yaml"): simpleKustomization, + secondDeploymentPath: secondDeploymentcontent, + filepath.Join("second-project", "kustomization.yaml"): componentKustomization, + } + buildGitRepo(t, gitDir, repoFiles) + expectedContents := map[string]string{ + "first-project": firstDeploymentcontent, + } + + require.NoError( + t, + kustomizeBuildDirs(outDir, false, []string{firstDeploymentPath, secondDeploymentPath}), + ) + compareResults(t, outDir, expectedContents, readOutDir(t, outDir)) +} + func TestSecretsStubbed(t *testing.T) { gitDir, outDir := setupTest(t) manifestsDir := filepath.Join("src", "manifests") diff --git a/go.mod b/go.mod index 9e1ed56..f21b060 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( github.com/urfave/cli/v2 v2.27.2 gitlab.com/matthewhughes/go-cov v0.4.0 golang.org/x/sync v0.7.0 - k8s.io/apimachinery v0.30.3 - k8s.io/client-go v0.30.3 + gopkg.in/yaml.v2 v2.4.0 + k8s.io/apimachinery v0.30.2 + k8s.io/client-go v0.30.2 sigs.k8s.io/controller-runtime v0.18.4 ) @@ -213,10 +214,9 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.4.7 // indirect - k8s.io/api v0.30.3 // indirect + k8s.io/api v0.30.2 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect diff --git a/go.sum b/go.sum index 28985c3..72126e9 100644 --- a/go.sum +++ b/go.sum @@ -618,14 +618,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= -k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= +k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= +k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= -k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= -k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= -k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= +k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= +k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= +k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=