Repository: Azure/draft
Pull request #326 (2024-06-28 19:05:01)

Rendering Kustomize Manifests in Code as Pre-Processor for go validate
# Description
Rendering Kustomize Manifests in Code as Pre-Processor for go validate

Fixes # (issue)
Feature # (details)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Is it a  breaking change which will impact consuming tool(s).

- TestRunValidate_Kustomize [x]
- TestRenderKustomizeManifest_Valid [x]


# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules



bacongobbler: We've learned a few things from using YAML in Helm. Here's a few war stories that justify why we are using TOML instead of other conventional markup languages commonly used in the Kubernetes community...

I can say from experience that debugging a YAML file written with improper or mixed indentation is the root of all evil, and has caused more developer frustrations than any other markup language. I can't count how many times I've seen Kubernetes users in Slack accidentally use tabs instead of spaces (and vice versa) and end up with obscure YAML parser errors. TOML does not care about indentation, which is a *really* welcome change for a simple markup language.

Secondly, the YAML spec is over 23,000 words. It's bloated, complex, and not at all "simple" or "obvious" to the user on its syntax. There are over [9 different ways to skin the proverbial cat](https://stackoverflow.com/a/21699210/2853424) in YAML, and because it is so complex most parsers will not support every feature. We (and users in general) only need 1 way to write a multi-line string. In TOML, it's just

```
message = """This is
a multi-line
string."""
```

Simple, right?

There is an excellent blog post by Martin Tournoij who explains the many intricacies and pitfalls of YAML, which I frequently point others to when asked this question: https://arp242.net/weblog/yaml_probably_not_so_great_after_all.html

> And what's wrong with JSON if you don't like YAML?

I love JSON and it certainly has its place, but it is not a human-readable markup language by any stretch. You can't write comments in JSON. That's a non-starter for a file that's meant to be used by developers to express how their application is deployed to production. At the end of the day, we're writing a library for developers, not machines. For projects that stand up infrastructure like [acs-engine](https://github.com/Azure/acs-engine) or [kops](https://github.com/kubernetes/kops), JSON is an excellent fit.

However, don't take my word for gospel. I'm genuinely interested in hearing your opinion on why we should use YAML instead of TOML. Perhaps my (no-so-subtle) jaded opinion can be swayed. I do not want to turn this discussion into a flamewar of "vim vs emacs", but in my opinion, there are features (or lack thereof) in TOML that make it seem like a much more fitting choice than YAML.
nikolay: Don't get me wrong! I don't like either - including HCL! YAML is bloated, JSON is limited, TOML looks nice (except its, by my taste, ugly double square brackets), but lacks a decent toolchain. For example, JSON might be limited, but there are jq, JMESPath, Jsonnet, and others. Anyway, it's not about the format, we can argue forever which one is better, but about the number of configuration formats in a single project.
bacongobbler: Closing this as answered. If there's technical limitations with TOML we can revisit this discussion.

Patch:
diff --git a/pkg/safeguards/helpers.go b/pkg/safeguards/helpers.go
index 1830127..c64874f 100644
--- a/pkg/safeguards/helpers.go
+++ b/pkg/safeguards/helpers.go
@@ -15,7 +15,6 @@ import (
 	"github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader"
 	"github.com/open-policy-agent/gatekeeper/v3/pkg/target"
 	log "github.com/sirupsen/logrus"
-
 	"golang.org/x/mod/semver"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/pkg/safeguards/preprocessing/preprocessing.go b/pkg/safeguards/preprocessing/preprocessing.go
new file mode 100644
index 0000000..4cc564b
--- /dev/null
+++ b/pkg/safeguards/preprocessing/preprocessing.go
@@ -0,0 +1,142 @@
+package preprocessing
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/Azure/draft/pkg/safeguards"
+	log "github.com/sirupsen/logrus"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chart/loader"
+	"helm.sh/helm/v3/pkg/engine"
+	"sigs.k8s.io/kustomize/api/krusty"
+	"sigs.k8s.io/kustomize/api/types"
+	"sigs.k8s.io/kustomize/kyaml/filesys"
+)
+
+// Given a Helm chart directory or file, renders all templates and writes them to the specified directory
+func RenderHelmChart(isFile bool, mainChartPath, tempDir string) ([]safeguards.ManifestFile, error) {
+	if isFile { // Get the directory that the Chart.yaml lives in
+		mainChartPath = filepath.Dir(mainChartPath)
+	}
+
+	mainChart, err := loader.Load(mainChartPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load main chart: %s", err)
+	}
+
+	loadedCharts := make(map[string]*chart.Chart) // map of chart path to chart object
+	loadedCharts[mainChartPath] = mainChart
+
+	// Load subcharts and dependencies
+	for _, dep := range mainChart.Metadata.Dependencies {
+		// Resolve the chart path based on the main chart's directory
+		chartPath := filepath.Join(mainChartPath, dep.Repository[len("file://"):])
+		chartPath = filepath.Clean(chartPath)
+
+		subChart, err := loader.Load(chartPath)
+		if err != nil {
+			return nil, fmt.Errorf("failed to load chart: %s", err)
+		}
+		loadedCharts[chartPath] = subChart
+	}
+
+	var manifestFiles []safeguards.ManifestFile
+	for chartPath, chart := range loadedCharts {
+		valuesPath := filepath.Join(chartPath, "values.yaml") // Enforce that values.yaml must be at same level as Chart.yaml
+		mergedValues, err := getValues(chart, valuesPath)
+		if err != nil {
+			return nil, fmt.Errorf("failed to load values: %s", err)
+		}
+		e := engine.Engine{Strict: true}
+		renderedFiles, err := e.Render(chart, mergedValues)
+		if err != nil {
+			return nil, fmt.Errorf("failed to render chart: %s", err)
+		}
+
+		// Write each rendered file to the output directory with the same name as in templates/
+		for renderedPath, content := range renderedFiles {
+			outputFilePath := filepath.Join(tempDir, filepath.Base(renderedPath))
+			if err := os.WriteFile(outputFilePath, []byte(content), 0644); err != nil {
+				return nil, fmt.Errorf("failed to write manifest file: %s", err)
+			}
+			manifestFiles = append(manifestFiles, safeguards.ManifestFile{Name: filepath.Base(renderedPath), Path: outputFilePath})
+		}
+	}
+
+	return manifestFiles, nil
+}
+
+// CreateTempDir creates a temporary directory on the user's file system for rendering templates
+func CreateTempDir(p string) error {
+	err := os.MkdirAll(p, 0755)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return err
+}
+
+// IsKustomize checks whether a given path should be treated as a kustomize project
+func IsKustomize(p string) bool {
+	var err error
+	if safeguards.IsYAML(p) {
+		return strings.Contains(p, "kustomization.yaml")
+	} else if _, err = os.Stat(filepath.Join(p, "kustomization.yaml")); err == nil {
+		return true
+	} else if _, err = os.Stat(filepath.Join(p, "kustomization.yml")); err == nil {
+		return true
+	}
+	return false
+}
+
+// Given a kustomization manifest file within kustomizationPath, RenderKustomizeManifest will render templates out to tempDir
+func RenderKustomizeManifest(kustomizationPath, tempDir string) ([]safeguards.ManifestFile, error) {
+	log.Debugf("Rendering kustomization.yaml...")
+	if safeguards.IsYAML(kustomizationPath) {
+		kustomizationPath = filepath.Dir(kustomizationPath)
+	}
+
+	options := &krusty.Options{
+		Reorder:          "none",
+		LoadRestrictions: types.LoadRestrictionsRootOnly,
+		PluginConfig:     &types.PluginConfig{},
+	}
+	k := krusty.MakeKustomizer(options)
+
+	// Run the build to generate the manifests
+	kustomizeFS := filesys.MakeFsOnDisk()
+	resMap, err := k.Run(kustomizeFS, kustomizationPath)
+	if err != nil {
+		return nil, fmt.Errorf("Error building manifests: %s\n", err.Error())
+	}
+
+	// Output the manifests
+	var manifestFiles []safeguards.ManifestFile
+	kindMap := make(map[string]int)
+	for _, res := range resMap.Resources() {
+		yamlRes, err := res.AsYAML()
+		if err != nil {
+			return nil, fmt.Errorf("Error converting resource to YAML: %s\n", err.Error())
+		}
+
+		// index of every kind of manifest for outputRenderPath
+		kindMap[res.GetKind()] += 1
+		outputRenderPath := filepath.Join(tempDir, strings.ToLower(res.GetKind())) + fmt.Sprintf("-%d.yaml", kindMap[res.GetKind()])
+
+		err = kustomizeFS.WriteFile(outputRenderPath, yamlRes)
+		if err != nil {
+			return nil, fmt.Errorf("Error writing yaml resource: %s\n", err.Error())
+		}
+
+		// write yamlRes to dir
+		manifestFiles = append(manifestFiles, safeguards.ManifestFile{
+			Name: res.GetName(),
+			Path: outputRenderPath,
+		})
+	}
+
+	return manifestFiles, nil
+}
diff --git a/pkg/safeguards/preprocessing/preprocessing_helpers.go b/pkg/safeguards/preprocessing/preprocessing_helpers.go
new file mode 100644
index 0000000..b08ac62
--- /dev/null
+++ b/pkg/safeguards/preprocessing/preprocessing_helpers.go
@@ -0,0 +1,54 @@
+package preprocessing
+
+import (
+	"fmt"
+	"os"
+
+	"gopkg.in/yaml.v3"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chartutil"
+)
+
+// Returns values from values.yaml and release options specified in values.yaml
+func getValues(chart *chart.Chart, valuesPath string) (chartutil.Values, error) {
+	// Load values file
+	valuesFile, err := os.ReadFile(valuesPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read values file: %s", err)
+	}
+
+	vals := map[string]interface{}{}
+	if err := yaml.Unmarshal(valuesFile, &vals); err != nil {
+		return nil, fmt.Errorf("failed to parse values.yaml: %s", err)
+	}
+
+	mergedValues, err := getReleaseOptions(chart, vals)
+	return mergedValues, err
+}
+
+func getReleaseOptions(chart *chart.Chart, vals map[string]interface{}) (chartutil.Values, error) {
+	// Extract release options from values
+	releaseName, ok := vals["releaseName"].(string)
+	if !ok || releaseName == "" {
+		return nil, fmt.Errorf("releaseName not found or empty in values.yaml")
+	}
+
+	releaseNamespace, ok := vals["releaseNamespace"].(string)
+	if !ok || releaseNamespace == "" {
+		return nil, fmt.Errorf("releaseNamespace not found or empty in values.yaml")
+	}
+
+	options := chartutil.ReleaseOptions{
+		Name:      releaseName,
+		Namespace: releaseNamespace,
+	}
+
+	// Combine chart values with release options
+	config := chartutil.Values(vals)
+	mergedValues, err := chartutil.ToRenderValues(chart, config, options, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to merge values: %s", err)
+	}
+
+	return mergedValues, nil
+}
diff --git a/pkg/safeguards/preprocessing_helpers.go b/pkg/safeguards/preprocessing_helpers.go
deleted file mode 100644
index 67adcdc..0000000
--- a/pkg/safeguards/preprocessing_helpers.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package safeguards
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-
-	"gopkg.in/yaml.v2"
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/chart/loader"
-	"helm.sh/helm/v3/pkg/chartutil"
-	"helm.sh/helm/v3/pkg/engine"
-)
-
-// Given a Helm chart directory or file, renders all templates and writes them to the specified directory
-func RenderHelmChart(isFile bool, mainChartPath, tempDir string) ([]ManifestFile, error) {
-	if isFile { // Get the directory that the Chart.yaml lives in
-		mainChartPath = filepath.Dir(mainChartPath)
-	}
-
-	mainChart, err := loader.Load(mainChartPath)
-	if err != nil {
-		return nil, fmt.Errorf("failed to load main chart: %s", err)
-	}
-
-	loadedCharts := make(map[string]*chart.Chart) // map of chart path to chart object
-	loadedCharts[mainChartPath] = mainChart
-
-	// Load subcharts and dependencies
-	for _, dep := range mainChart.Metadata.Dependencies {
-		// Resolve the chart path based on the main chart's directory
-		chartPath := filepath.Join(mainChartPath, dep.Repository[len("file://"):])
-		chartPath = filepath.Clean(chartPath)
-
-		subChart, err := loader.Load(chartPath)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load chart: %s", err)
-		}
-		loadedCharts[chartPath] = subChart
-	}
-
-	var manifestFiles []ManifestFile
-	for chartPath, chart := range loadedCharts {
-		valuesPath := filepath.Join(chartPath, "values.yaml") // Enforce that values.yaml must be at same level as Chart.yaml
-		mergedValues, err := getValues(chart, valuesPath)
-		if err != nil {
-			return nil, fmt.Errorf("failed to load values: %s", err)
-		}
-		e := engine.Engine{Strict: true}
-		renderedFiles, err := e.Render(chart, mergedValues)
-		if err != nil {
-			return nil, fmt.Errorf("failed to render chart: %s", err)
-		}
-
-		// Write each rendered file to the output directory with the same name as in templates/
-		for filePath, content := range renderedFiles {
-			outputFilePath := filepath.Join(tempDir, filepath.Base(filePath))
-			if err := os.WriteFile(outputFilePath, []byte(content), 0644); err != nil {
-				return nil, fmt.Errorf("failed to write manifest file: %s", err)
-			}
-			manifestFiles = append(manifestFiles, ManifestFile{Name: filepath.Base(filePath), Path: outputFilePath})
-		}
-	}
-
-	return manifestFiles, nil
-}
-
-// Returns values from values.yaml and release options specified in values.yaml
-func getValues(chart *chart.Chart, valuesPath string) (chartutil.Values, error) {
-	// Load values file
-	valuesFile, err := os.ReadFile(valuesPath)
-	if err != nil {
-		return nil, fmt.Errorf("failed to read values file: %s", err)
-	}
-
-	vals := map[string]interface{}{}
-	if err := yaml.Unmarshal(valuesFile, &vals); err != nil {
-		return nil, fmt.Errorf("failed to parse values.yaml: %s", err)
-	}
-
-	mergedValues, err := getReleaseOptions(chart, vals)
-	return mergedValues, err
-}
-
-func getReleaseOptions(chart *chart.Chart, vals map[string]interface{}) (chartutil.Values, error) {
-	// Extract release options from values
-	releaseName, ok := vals["releaseName"].(string)
-	if !ok || releaseName == "" {
-		return nil, fmt.Errorf("releaseName not found or empty in values.yaml")
-	}
-
-	releaseNamespace, ok := vals["releaseNamespace"].(string)
-	if !ok || releaseNamespace == "" {
-		return nil, fmt.Errorf("releaseNamespace not found or empty in values.yaml")
-	}
-
-	options := chartutil.ReleaseOptions{
-		Name:      releaseName,
-		Namespace: releaseNamespace,
-	}
-
-	// Combine chart values with release options
-	config := chartutil.Values(vals)
-	mergedValues, err := chartutil.ToRenderValues(chart, config, options, nil)
-	if err != nil {
-		return nil, fmt.Errorf("failed to merge values: %s", err)
-	}
-
-	return mergedValues, nil
-}


Test patch:
diff --git a/cmd/validate_test.go b/cmd/validate_test.go
index d6fd91a..c61c605 100644
--- a/cmd/validate_test.go
+++ b/cmd/validate_test.go
@@ -9,6 +9,7 @@ import (
 	"testing"
 
 	"github.com/Azure/draft/pkg/safeguards"
+	"github.com/Azure/draft/pkg/safeguards/preprocessing"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -95,3 +96,32 @@ func TestRunValidate(t *testing.T) {
 	numViolations = countTestViolations(v)
 	assert.Greater(t, numViolations, 0)
 }
+
+// TestRunValidate_Kustomize tests the run command for `draft validate` for proper returns when given a kustomize project
+func TestRunValidate_Kustomize(t *testing.T) {
+	ctx := context.TODO()
+	kustomizationPath, _ := filepath.Abs("../pkg/safeguards/tests/kustomize/overlays/production")
+	kustomizationFilePath, _ := filepath.Abs("../pkg/safeguards/tests/kustomize/overlays/production/kustomization.yaml")
+
+	makeTempDir(t)
+	t.Cleanup(func() { cleanupDir(t, tempDir) })
+
+	var manifestFiles []safeguards.ManifestFile
+	var err error
+
+	// Scenario 1a: kustomizationPath leads to a directory containing kustomization.yaml - expect success
+	manifestFiles, err = preprocessing.RenderKustomizeManifest(kustomizationPath, tempDir)
+	assert.Nil(t, err)
+	v, err := safeguards.GetManifestResults(ctx, manifestFiles)
+	assert.Nil(t, err)
+	numViolations := countTestViolations(v)
+	assert.Equal(t, numViolations, 1)
+
+	// Scenario 1b: kustomizationFilePath path leads to a specific kustomization.yaml - expect success
+	manifestFiles, err = preprocessing.RenderKustomizeManifest(kustomizationFilePath, tempDir)
+	assert.Nil(t, err)
+	v, err = safeguards.GetManifestResults(ctx, manifestFiles)
+	assert.Nil(t, err)
+	numViolations = countTestViolations(v)
+	assert.Equal(t, numViolations, 1)
+}
diff --git a/cmd/validate_test_helpers.go b/cmd/validate_test_helpers.go
index 4fd638b..0109fe9 100644
--- a/cmd/validate_test_helpers.go
+++ b/cmd/validate_test_helpers.go
@@ -1,6 +1,14 @@
 package cmd
 
-import "github.com/Azure/draft/pkg/safeguards"
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/Azure/draft/pkg/safeguards"
+)
+
+var tempDir, _ = filepath.Abs("./testdata")
 
 func countTestViolations(results []safeguards.ManifestResult) int {
 	numViolations := 0
@@ -10,3 +18,16 @@ func countTestViolations(results []safeguards.ManifestResult) int {
 
 	return numViolations
 }
+
+func makeTempDir(t *testing.T) {
+	if err := os.MkdirAll(tempDir, 0755); err != nil {
+		t.Fatalf("failed to create temporary output directory: %s", err)
+	}
+}
+
+func cleanupDir(t *testing.T, dir string) {
+	err := os.RemoveAll(dir)
+	if err != nil {
+		t.Fatalf("Failed to clean directory: %s", err)
+	}
+}
diff --git a/pkg/safeguards/preprocessing_helpers_test.go b/pkg/safeguards/preprocessing/preprocessing_test.go
similarity index 59%
rename from pkg/safeguards/preprocessing_helpers_test.go
rename to pkg/safeguards/preprocessing/preprocessing_test.go
index c9ef420..1afe50f 100644
--- a/pkg/safeguards/preprocessing_helpers_test.go
+++ b/pkg/safeguards/preprocessing/preprocessing_test.go
@@ -1,4 +1,4 @@
-package safeguards
+package preprocessing
 
 import (
 	"os"
@@ -6,36 +6,8 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"gopkg.in/yaml.v2"
 )
 
-const (
-	tempDir                 = "testdata" // Rendered files are stored here before they are read for comparison
-	chartPath               = "tests/testmanifests/validchart"
-	invalidChartPath        = "tests/testmanifests/invalidchart"
-	invalidValuesChart      = "tests/testmanifests/invalidvalues"
-	invalidDeploymentsChart = "tests/testmanifests/invaliddeployment"
-	invalidDeploymentSyntax = "tests/testmanifests/invaliddeployment-syntax"
-	invalidDeploymentValues = "tests/testmanifests/invaliddeployment-values"
-	folderwithHelpersTmpl   = "tests/testmanifests/different-structure"
-	multipleTemplateDirs    = "tests/testmanifests/multiple-templates"
-	multipleValuesFile      = "tests/testmanifests/multiple-values-files"
-
-	subcharts                  = "tests/testmanifests/multiple-charts"
-	subchartDir                = "tests/testmanifests/multiple-charts/charts/subchart2"
-	directPath_ToSubchartYaml  = "tests/testmanifests/multiple-charts/charts/subchart1/Chart.yaml"
-	directPath_ToMainChartYaml = "tests/testmanifests/multiple-charts/Chart.yaml"
-
-	directPath_ToValidChart   = "tests/testmanifests/validchart/Chart.yaml"
-	directPath_ToInvalidChart = "tests/testmanifests/invalidchart/Chart.yaml"
-)
-
-func makeTempDir(t *testing.T) {
-	if err := os.MkdirAll(tempDir, 0755); err != nil {
-		t.Fatalf("failed to create temporary output directory: %s", err)
-	}
-}
-
 // Test rendering a valid Helm chart with no subcharts and three templates
 func TestRenderHelmChart_Valid(t *testing.T) {
 	makeTempDir(t)
@@ -46,9 +18,9 @@ func TestRenderHelmChart_Valid(t *testing.T) {
 
 	// Check that the output directory exists and contains expected files
 	expectedFiles := make(map[string]string)
-	expectedFiles["deployment.yaml"] = getManifestAsString(t, "tests/testmanifests/expecteddeployment.yaml")
-	expectedFiles["service.yaml"] = getManifestAsString(t, "tests/testmanifests/expectedservice.yaml")
-	expectedFiles["ingress.yaml"] = getManifestAsString(t, "tests/testmanifests/expectedingress.yaml")
+	expectedFiles["deployment.yaml"] = getManifestAsString(t, "../tests/testmanifests/expecteddeployment.yaml")
+	expectedFiles["service.yaml"] = getManifestAsString(t, "../tests/testmanifests/expectedservice.yaml")
+	expectedFiles["ingress.yaml"] = getManifestAsString(t, "../tests/testmanifests/expectedingress.yaml")
 
 	for _, writtenFile := range manifestFiles {
 		expectedYaml := expectedFiles[writtenFile.Name]
@@ -83,9 +55,9 @@ func TestSubCharts(t *testing.T) {
 	assert.Equal(t, len(files), 3)
 
 	expectedFiles := make(map[string]string)
-	expectedFiles["maindeployment.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-mainchart.yaml")
-	expectedFiles["deployment1.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-subchart1.yaml")
-	expectedFiles["deployment2.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-subchart2.yaml")
+	expectedFiles["maindeployment.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-mainchart.yaml")
+	expectedFiles["deployment1.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-subchart1.yaml")
+	expectedFiles["deployment2.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-subchart2.yaml")
 
 	for _, writtenFile := range manifestFiles {
 		expectedYaml := expectedFiles[writtenFile.Name]
@@ -110,13 +82,13 @@ func TestSubCharts(t *testing.T) {
 	cleanupDir(t, tempDir)
 	makeTempDir(t)
 
-	// Given path to a sub- Chart.yaml with a dependency on another subchart, should render both subcharts, but not the main chart
+	// Given path to a sub-Chart.yaml with a dependency on another subchart, should render both subcharts, but not the main chart
 	manifestFiles, err = RenderHelmChart(true, directPath_ToSubchartYaml, tempDir)
 	assert.Nil(t, err)
 
 	expectedFiles = make(map[string]string)
-	expectedFiles["deployment1.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-subchart1.yaml")
-	expectedFiles["deployment2.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-subchart2.yaml")
+	expectedFiles["deployment1.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-subchart1.yaml")
+	expectedFiles["deployment2.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-subchart2.yaml")
 
 	for _, writtenFile := range manifestFiles {
 		expectedYaml := expectedFiles[writtenFile.Name]
@@ -176,8 +148,8 @@ func TestDifferentFolderStructures(t *testing.T) {
 	assert.Nil(t, err)
 
 	expectedFiles := make(map[string]string)
-	expectedFiles["deployment.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-helpers-deployment.yaml")
-	expectedFiles["service.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-helpers-service.yaml")
+	expectedFiles["deployment.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-helpers-deployment.yaml")
+	expectedFiles["service.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-helpers-service.yaml")
 	for _, writtenFile := range manifestFiles {
 		expectedYaml := expectedFiles[writtenFile.Name]
 		writtenYaml := parseYAML(t, getManifestAsString(t, writtenFile.Path))
@@ -190,9 +162,9 @@ func TestDifferentFolderStructures(t *testing.T) {
 	assert.Nil(t, err)
 
 	expectedFiles = make(map[string]string)
-	expectedFiles["resources.yaml"] = getManifestAsString(t, "tests/testmanifests/expected-resources.yaml")
-	expectedFiles["service-1.yaml"] = getManifestAsString(t, "tests/testmanifests/expectedservice.yaml")
-	expectedFiles["service-2.yaml"] = getManifestAsString(t, "tests/testmanifests/expectedservice2.yaml")
+	expectedFiles["resources.yaml"] = getManifestAsString(t, "../tests/testmanifests/expected-resources.yaml")
+	expectedFiles["service-1.yaml"] = getManifestAsString(t, "../tests/testmanifests/expectedservice.yaml")
+	expectedFiles["service-2.yaml"] = getManifestAsString(t, "../tests/testmanifests/expectedservice2.yaml")
 	for _, writtenFile := range manifestFiles {
 		expectedYaml := expectedFiles[writtenFile.Name]
 		writtenYaml := parseYAML(t, getManifestAsString(t, writtenFile.Path))
@@ -200,28 +172,24 @@ func TestDifferentFolderStructures(t *testing.T) {
 	}
 }
 
-func cleanupDir(t *testing.T, dir string) {
-	err := os.RemoveAll(dir)
-	if err != nil {
-		t.Fatalf("Failed to clean directory: %s", err)
-	}
-}
+// Test rendering a valid kustomization.yaml
+func TestRenderKustomizeManifest_Valid(t *testing.T) {
+	makeTempDir(t)
+	t.Cleanup(func() { cleanupDir(t, tempDir) })
 
-func parseYAML(t *testing.T, content string) map[string]interface{} {
-	var result map[string]interface{}
-	err := yaml.Unmarshal([]byte(content), &result)
-	if err != nil {
-		t.Fatalf("Failed to parse YAML: %s", err)
-	}
-	return result
+	_, err := RenderKustomizeManifest(kustomizationPath, tempDir)
+	assert.Nil(t, err)
 }
 
-func getManifestAsString(t *testing.T, filePath string) string {
-	yamlFileContent, err := os.ReadFile(filePath)
-	if err != nil {
-		t.Fatalf("Failed to read YAML file: %s", err)
-	}
-
-	yamlContentString := string(yamlFileContent)
-	return yamlContentString
+// TestIsKustomize checks whether the given path contains a kustomize project
+func TestIsKustomize(t *testing.T) {
+	// path contains a kustomization.yaml file
+	isKustomize := IsKustomize(kustomizationPath)
+	assert.True(t, isKustomize)
+	// path is a kustomization.yaml file
+	isKustomize = IsKustomize(kustomizationFilePath)
+	assert.True(t, isKustomize)
+	// not a kustomize project
+	isKustomize = IsKustomize(chartPath)
+	assert.False(t, isKustomize)
 }
diff --git a/pkg/safeguards/preprocessing/preprocessing_test_helpers.go b/pkg/safeguards/preprocessing/preprocessing_test_helpers.go
new file mode 100644
index 0000000..ff8061b
--- /dev/null
+++ b/pkg/safeguards/preprocessing/preprocessing_test_helpers.go
@@ -0,0 +1,64 @@
+package preprocessing
+
+import (
+	"os"
+	"testing"
+
+	"gopkg.in/yaml.v3"
+)
+
+const (
+	tempDir                 = "testdata" // Rendered files are stored here before they are read for comparison
+	chartPath               = "../tests/testmanifests/validchart"
+	invalidChartPath        = "../tests/testmanifests/invalidchart"
+	invalidValuesChart      = "../tests/testmanifests/invalidvalues"
+	invalidDeploymentsChart = "../tests/testmanifests/invaliddeployment"
+	invalidDeploymentSyntax = "../tests/testmanifests/invaliddeployment-syntax"
+	invalidDeploymentValues = "../tests/testmanifests/invaliddeployment-values"
+	folderwithHelpersTmpl   = "../tests/testmanifests/different-structure"
+	multipleTemplateDirs    = "../tests/testmanifests/multiple-templates"
+	multipleValuesFile      = "../tests/testmanifests/multiple-values-files"
+
+	subcharts                  = "../tests/testmanifests/multiple-charts"
+	subchartDir                = "../tests/testmanifests/multiple-charts/charts/subchart2"
+	directPath_ToSubchartYaml  = "../tests/testmanifests/multiple-charts/charts/subchart1/Chart.yaml"
+	directPath_ToMainChartYaml = "../tests/testmanifests/multiple-charts/Chart.yaml"
+
+	directPath_ToValidChart   = "../tests/testmanifests/validchart/Chart.yaml"
+	directPath_ToInvalidChart = "../tests/testmanifests/invalidchart/Chart.yaml"
+
+	kustomizationPath     = "../tests/kustomize/overlays/production"
+	kustomizationFilePath = "../tests/kustomize/overlays/production/kustomization.yaml"
+)
+
+func makeTempDir(t *testing.T) {
+	if err := CreateTempDir(tempDir); err != nil {
+		t.Fatalf("failed to create temporary output directory: %s", err)
+	}
+}
+
+func cleanupDir(t *testing.T, dir string) {
+	err := os.RemoveAll(dir)
+	if err != nil {
+		t.Fatalf("Failed to clean directory: %s", err)
+	}
+}
+
+func parseYAML(t *testing.T, content string) map[string]interface{} {
+	var result map[string]interface{}
+	err := yaml.Unmarshal([]byte(content), &result)
+	if err != nil {
+		t.Fatalf("Failed to parse YAML: %s", err)
+	}
+	return result
+}
+
+func getManifestAsString(t *testing.T, filePath string) string {
+	yamlFileContent, err := os.ReadFile(filePath)
+	if err != nil {
+		t.Fatalf("Failed to read YAML file: %s", err)
+	}
+
+	yamlContentString := string(yamlFileContent)
+	return yamlContentString
+}
diff --git a/pkg/safeguards/testdata/resources.yaml b/pkg/safeguards/testdata/resources.yaml
deleted file mode 100644
index 6a2ffe2..0000000
--- a/pkg/safeguards/testdata/resources.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: test-release-deployment
-  namespace: test-namespace
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: my-web-app
-  template:
-    metadata:
-      labels:
-        app: my-web-app
-    spec:
-      containers:
-        - name: nginx
-          image: nginx:stable
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: test-release-configmap
-  namespace: test-namespace
-data:
-  config.yaml: |
-    replicaCount: 1
-    image:
-      repository: nginx
-      tag: stable
-      pullPolicy: IfNotPresent
-    service:
-      type: ClusterIP
-      port: 80
-    ingress:
-      enabled: true
-      hostname: example.com
-      annotations:
-      tls: false
-      tlsSecret: 
-
diff --git a/pkg/safeguards/testdata/service-1.yaml b/pkg/safeguards/testdata/service-1.yaml
deleted file mode 100644
index d22f2b4..0000000
--- a/pkg/safeguards/testdata/service-1.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: test-release-service
-  namespace: test-namespace
-spec:
-  type: ClusterIP
-  ports:
-    - port: 80
-  selector:
-    app: my-web-app
\ No newline at end of file
diff --git a/pkg/safeguards/testdata/service-2.yaml b/pkg/safeguards/testdata/service-2.yaml
deleted file mode 100644
index e5f2a45..0000000
--- a/pkg/safeguards/testdata/service-2.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: test-release-service2
-  namespace: test-namespace
-spec:
-  type: ClusterIP
-  ports:
-    - port: 80
-  selector:
-    app: my-web-app
\ No newline at end of file
diff --git a/pkg/safeguards/tests/kustomize/base/deployment.yaml b/pkg/safeguards/tests/kustomize/base/deployment.yaml
new file mode 100644
index 0000000..3da0d20
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/base/deployment.yaml
@@ -0,0 +1,23 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: my-app
+  labels:
+    app: my-app
+  namespace: my-namespace
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: my-app
+  template:
+    metadata:
+      labels:
+        app: my-app
+    spec:
+      containers:
+        - name: my-app
+          image: nginx:latest
+          imagePullPolicy: Always
+          ports:
+            - containerPort: 80
diff --git a/pkg/safeguards/tests/kustomize/base/kustomization.yaml b/pkg/safeguards/tests/kustomize/base/kustomization.yaml
new file mode 100644
index 0000000..ca7c5a9
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/base/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+- deployment.yaml
+- service.yaml
+- namespace.yaml
diff --git a/pkg/safeguards/tests/kustomize/base/namespace.yaml b/pkg/safeguards/tests/kustomize/base/namespace.yaml
new file mode 100644
index 0000000..4c1590e
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/base/namespace.yaml
@@ -0,0 +1,4 @@
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: my-namespace
\ No newline at end of file
diff --git a/pkg/safeguards/tests/kustomize/base/service.yaml b/pkg/safeguards/tests/kustomize/base/service.yaml
new file mode 100644
index 0000000..8de1e8a
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/base/service.yaml
@@ -0,0 +1,13 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-service
+  namespace: my-namespace
+spec:
+  type: LoadBalancer
+  selector:
+    app: my-app
+  ports:
+    - protocol: TCP
+      port: 4040
+      targetPort: 80
\ No newline at end of file
diff --git a/pkg/safeguards/tests/kustomize/overlays/production/deployment.yaml b/pkg/safeguards/tests/kustomize/overlays/production/deployment.yaml
new file mode 100644
index 0000000..51c8873
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/overlays/production/deployment.yaml
@@ -0,0 +1,16 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: my-app
+  labels:
+    app: my-app
+  namespace: my-namespace
+spec:
+  selector:
+    matchLabels:
+      app: my-app
+  template:
+    spec:
+      containers:
+        - name: my-app
+          image: nginx:latest
\ No newline at end of file
diff --git a/pkg/safeguards/tests/kustomize/overlays/production/kustomization.yaml b/pkg/safeguards/tests/kustomize/overlays/production/kustomization.yaml
new file mode 100644
index 0000000..8ca4ec3
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/overlays/production/kustomization.yaml
@@ -0,0 +1,10 @@
+namePrefix: production-
+namespace: my-namespace
+resources:
+- ../../base
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+patches:
+- path: deployment.yaml
+- path: service.yaml
+- path: namespace.yaml
diff --git a/pkg/safeguards/tests/kustomize/overlays/production/namespace.yaml b/pkg/safeguards/tests/kustomize/overlays/production/namespace.yaml
new file mode 100644
index 0000000..4c1590e
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/overlays/production/namespace.yaml
@@ -0,0 +1,4 @@
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: my-namespace
\ No newline at end of file
diff --git a/pkg/safeguards/tests/kustomize/overlays/production/service.yaml b/pkg/safeguards/tests/kustomize/overlays/production/service.yaml
new file mode 100644
index 0000000..3d18a97
--- /dev/null
+++ b/pkg/safeguards/tests/kustomize/overlays/production/service.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: my-service
+  namespace: my-namespace
+spec:
+  type: LoadBalancer
diff --git a/pkg/safeguards/tests/testmanifests/expected-helpers-deployment.yaml b/pkg/safeguards/tests/testmanifests/expected-helpers-deployment.yaml
index 20ea5f3..7ca6198 100644
--- a/pkg/safeguards/tests/testmanifests/expected-helpers-deployment.yaml
+++ b/pkg/safeguards/tests/testmanifests/expected-helpers-deployment.yaml
@@ -3,7 +3,7 @@ kind: Deployment
 metadata:
   name: test-release-mychart
   labels:
-    
+
     helm.sh/chart: mychart-0.1.0
     test-release-mychart: test-release
     app.kubernetes.io/managed-by: Helm
@@ -27,4 +27,4 @@ spec:
           ports:
             - containerPort: 80
           resources:
-            
+            {}