Skip to content

Commit 6e0b1ad

Browse files
authored
Rendering Kustomize Manifests in Code as Pre-Processor for go validate (#326)
1 parent 96f500a commit 6e0b1ad

20 files changed

+429
-241
lines changed

cmd/validate_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/Azure/draft/pkg/safeguards"
12+
"github.com/Azure/draft/pkg/safeguards/preprocessing"
1213
"github.com/stretchr/testify/assert"
1314
)
1415

@@ -95,3 +96,32 @@ func TestRunValidate(t *testing.T) {
9596
numViolations = countTestViolations(v)
9697
assert.Greater(t, numViolations, 0)
9798
}
99+
100+
// TestRunValidate_Kustomize tests the run command for `draft validate` for proper returns when given a kustomize project
101+
func TestRunValidate_Kustomize(t *testing.T) {
102+
ctx := context.TODO()
103+
kustomizationPath, _ := filepath.Abs("../pkg/safeguards/tests/kustomize/overlays/production")
104+
kustomizationFilePath, _ := filepath.Abs("../pkg/safeguards/tests/kustomize/overlays/production/kustomization.yaml")
105+
106+
makeTempDir(t)
107+
t.Cleanup(func() { cleanupDir(t, tempDir) })
108+
109+
var manifestFiles []safeguards.ManifestFile
110+
var err error
111+
112+
// Scenario 1a: kustomizationPath leads to a directory containing kustomization.yaml - expect success
113+
manifestFiles, err = preprocessing.RenderKustomizeManifest(kustomizationPath, tempDir)
114+
assert.Nil(t, err)
115+
v, err := safeguards.GetManifestResults(ctx, manifestFiles)
116+
assert.Nil(t, err)
117+
numViolations := countTestViolations(v)
118+
assert.Equal(t, numViolations, 1)
119+
120+
// Scenario 1b: kustomizationFilePath path leads to a specific kustomization.yaml - expect success
121+
manifestFiles, err = preprocessing.RenderKustomizeManifest(kustomizationFilePath, tempDir)
122+
assert.Nil(t, err)
123+
v, err = safeguards.GetManifestResults(ctx, manifestFiles)
124+
assert.Nil(t, err)
125+
numViolations = countTestViolations(v)
126+
assert.Equal(t, numViolations, 1)
127+
}

cmd/validate_test_helpers.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
package cmd
22

3-
import "github.com/Azure/draft/pkg/safeguards"
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/Azure/draft/pkg/safeguards"
9+
)
10+
11+
var tempDir, _ = filepath.Abs("./testdata")
412

513
func countTestViolations(results []safeguards.ManifestResult) int {
614
numViolations := 0
@@ -10,3 +18,16 @@ func countTestViolations(results []safeguards.ManifestResult) int {
1018

1119
return numViolations
1220
}
21+
22+
func makeTempDir(t *testing.T) {
23+
if err := os.MkdirAll(tempDir, 0755); err != nil {
24+
t.Fatalf("failed to create temporary output directory: %s", err)
25+
}
26+
}
27+
28+
func cleanupDir(t *testing.T, dir string) {
29+
err := os.RemoveAll(dir)
30+
if err != nil {
31+
t.Fatalf("Failed to clean directory: %s", err)
32+
}
33+
}

pkg/safeguards/helpers.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/open-policy-agent/gatekeeper/v3/pkg/gator/reader"
1616
"github.com/open-policy-agent/gatekeeper/v3/pkg/target"
1717
log "github.com/sirupsen/logrus"
18-
1918
"golang.org/x/mod/semver"
2019
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2120
"k8s.io/apimachinery/pkg/runtime"
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package preprocessing
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/Azure/draft/pkg/safeguards"
10+
log "github.com/sirupsen/logrus"
11+
"helm.sh/helm/v3/pkg/chart"
12+
"helm.sh/helm/v3/pkg/chart/loader"
13+
"helm.sh/helm/v3/pkg/engine"
14+
"sigs.k8s.io/kustomize/api/krusty"
15+
"sigs.k8s.io/kustomize/api/types"
16+
"sigs.k8s.io/kustomize/kyaml/filesys"
17+
)
18+
19+
// Given a Helm chart directory or file, renders all templates and writes them to the specified directory
20+
func RenderHelmChart(isFile bool, mainChartPath, tempDir string) ([]safeguards.ManifestFile, error) {
21+
if isFile { // Get the directory that the Chart.yaml lives in
22+
mainChartPath = filepath.Dir(mainChartPath)
23+
}
24+
25+
mainChart, err := loader.Load(mainChartPath)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to load main chart: %s", err)
28+
}
29+
30+
loadedCharts := make(map[string]*chart.Chart) // map of chart path to chart object
31+
loadedCharts[mainChartPath] = mainChart
32+
33+
// Load subcharts and dependencies
34+
for _, dep := range mainChart.Metadata.Dependencies {
35+
// Resolve the chart path based on the main chart's directory
36+
chartPath := filepath.Join(mainChartPath, dep.Repository[len("file://"):])
37+
chartPath = filepath.Clean(chartPath)
38+
39+
subChart, err := loader.Load(chartPath)
40+
if err != nil {
41+
return nil, fmt.Errorf("failed to load chart: %s", err)
42+
}
43+
loadedCharts[chartPath] = subChart
44+
}
45+
46+
var manifestFiles []safeguards.ManifestFile
47+
for chartPath, chart := range loadedCharts {
48+
valuesPath := filepath.Join(chartPath, "values.yaml") // Enforce that values.yaml must be at same level as Chart.yaml
49+
mergedValues, err := getValues(chart, valuesPath)
50+
if err != nil {
51+
return nil, fmt.Errorf("failed to load values: %s", err)
52+
}
53+
e := engine.Engine{Strict: true}
54+
renderedFiles, err := e.Render(chart, mergedValues)
55+
if err != nil {
56+
return nil, fmt.Errorf("failed to render chart: %s", err)
57+
}
58+
59+
// Write each rendered file to the output directory with the same name as in templates/
60+
for renderedPath, content := range renderedFiles {
61+
outputFilePath := filepath.Join(tempDir, filepath.Base(renderedPath))
62+
if err := os.WriteFile(outputFilePath, []byte(content), 0644); err != nil {
63+
return nil, fmt.Errorf("failed to write manifest file: %s", err)
64+
}
65+
manifestFiles = append(manifestFiles, safeguards.ManifestFile{Name: filepath.Base(renderedPath), Path: outputFilePath})
66+
}
67+
}
68+
69+
return manifestFiles, nil
70+
}
71+
72+
// CreateTempDir creates a temporary directory on the user's file system for rendering templates
73+
func CreateTempDir(p string) error {
74+
err := os.MkdirAll(p, 0755)
75+
if err != nil {
76+
log.Fatal(err)
77+
}
78+
79+
return err
80+
}
81+
82+
// IsKustomize checks whether a given path should be treated as a kustomize project
83+
func IsKustomize(p string) bool {
84+
var err error
85+
if safeguards.IsYAML(p) {
86+
return strings.Contains(p, "kustomization.yaml")
87+
} else if _, err = os.Stat(filepath.Join(p, "kustomization.yaml")); err == nil {
88+
return true
89+
} else if _, err = os.Stat(filepath.Join(p, "kustomization.yml")); err == nil {
90+
return true
91+
}
92+
return false
93+
}
94+
95+
// Given a kustomization manifest file within kustomizationPath, RenderKustomizeManifest will render templates out to tempDir
96+
func RenderKustomizeManifest(kustomizationPath, tempDir string) ([]safeguards.ManifestFile, error) {
97+
log.Debugf("Rendering kustomization.yaml...")
98+
if safeguards.IsYAML(kustomizationPath) {
99+
kustomizationPath = filepath.Dir(kustomizationPath)
100+
}
101+
102+
options := &krusty.Options{
103+
Reorder: "none",
104+
LoadRestrictions: types.LoadRestrictionsRootOnly,
105+
PluginConfig: &types.PluginConfig{},
106+
}
107+
k := krusty.MakeKustomizer(options)
108+
109+
// Run the build to generate the manifests
110+
kustomizeFS := filesys.MakeFsOnDisk()
111+
resMap, err := k.Run(kustomizeFS, kustomizationPath)
112+
if err != nil {
113+
return nil, fmt.Errorf("Error building manifests: %s\n", err.Error())
114+
}
115+
116+
// Output the manifests
117+
var manifestFiles []safeguards.ManifestFile
118+
kindMap := make(map[string]int)
119+
for _, res := range resMap.Resources() {
120+
yamlRes, err := res.AsYAML()
121+
if err != nil {
122+
return nil, fmt.Errorf("Error converting resource to YAML: %s\n", err.Error())
123+
}
124+
125+
// index of every kind of manifest for outputRenderPath
126+
kindMap[res.GetKind()] += 1
127+
outputRenderPath := filepath.Join(tempDir, strings.ToLower(res.GetKind())) + fmt.Sprintf("-%d.yaml", kindMap[res.GetKind()])
128+
129+
err = kustomizeFS.WriteFile(outputRenderPath, yamlRes)
130+
if err != nil {
131+
return nil, fmt.Errorf("Error writing yaml resource: %s\n", err.Error())
132+
}
133+
134+
// write yamlRes to dir
135+
manifestFiles = append(manifestFiles, safeguards.ManifestFile{
136+
Name: res.GetName(),
137+
Path: outputRenderPath,
138+
})
139+
}
140+
141+
return manifestFiles, nil
142+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package preprocessing
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"gopkg.in/yaml.v3"
8+
"helm.sh/helm/v3/pkg/chart"
9+
"helm.sh/helm/v3/pkg/chartutil"
10+
)
11+
12+
// Returns values from values.yaml and release options specified in values.yaml
13+
func getValues(chart *chart.Chart, valuesPath string) (chartutil.Values, error) {
14+
// Load values file
15+
valuesFile, err := os.ReadFile(valuesPath)
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to read values file: %s", err)
18+
}
19+
20+
vals := map[string]interface{}{}
21+
if err := yaml.Unmarshal(valuesFile, &vals); err != nil {
22+
return nil, fmt.Errorf("failed to parse values.yaml: %s", err)
23+
}
24+
25+
mergedValues, err := getReleaseOptions(chart, vals)
26+
return mergedValues, err
27+
}
28+
29+
func getReleaseOptions(chart *chart.Chart, vals map[string]interface{}) (chartutil.Values, error) {
30+
// Extract release options from values
31+
releaseName, ok := vals["releaseName"].(string)
32+
if !ok || releaseName == "" {
33+
return nil, fmt.Errorf("releaseName not found or empty in values.yaml")
34+
}
35+
36+
releaseNamespace, ok := vals["releaseNamespace"].(string)
37+
if !ok || releaseNamespace == "" {
38+
return nil, fmt.Errorf("releaseNamespace not found or empty in values.yaml")
39+
}
40+
41+
options := chartutil.ReleaseOptions{
42+
Name: releaseName,
43+
Namespace: releaseNamespace,
44+
}
45+
46+
// Combine chart values with release options
47+
config := chartutil.Values(vals)
48+
mergedValues, err := chartutil.ToRenderValues(chart, config, options, nil)
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to merge values: %s", err)
51+
}
52+
53+
return mergedValues, nil
54+
}

0 commit comments

Comments
 (0)