forked from akuity/kargo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement
kustomize build
directive
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
- Loading branch information
Showing
6 changed files
with
266 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package directives | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"sync" | ||
|
||
securejoin "github.com/cyphar/filepath-securejoin" | ||
securefs "github.com/fluxcd/pkg/kustomize/filesys" | ||
"github.com/xeipuuv/gojsonschema" | ||
"sigs.k8s.io/kustomize/api/krusty" | ||
"sigs.k8s.io/kustomize/api/resmap" | ||
kustypes "sigs.k8s.io/kustomize/api/types" | ||
"sigs.k8s.io/kustomize/kyaml/filesys" | ||
) | ||
|
||
// kustomizeRenderMutex is a mutex that ensures only one kustomize build is | ||
// running at a time. Required because of an ancient bug in Kustomize that | ||
// causes it to concurrently read and write to the same map, causing a panic. | ||
// xref: https://github.com/kubernetes-sigs/kustomize/issues/3659 | ||
var kustomizeRenderMutex sync.Mutex | ||
|
||
func init() { | ||
// Register the kustomize-build directive with the builtins registry. | ||
builtins.RegisterDirective(newKustomizeBuildDirective(), nil) | ||
} | ||
|
||
// kustomizeBuildDirective is a directive that builds a set of Kubernetes | ||
// manifests using Kustomize. | ||
type kustomizeBuildDirective struct { | ||
schemaLoader gojsonschema.JSONLoader | ||
} | ||
|
||
// newKustomizeBuildDirective creates a new kustomize-build directive. | ||
func newKustomizeBuildDirective() Directive { | ||
return &kustomizeBuildDirective{ | ||
schemaLoader: getConfigSchemaLoader("kustomize-build"), | ||
} | ||
} | ||
|
||
// Name implements the Directive interface. | ||
func (d *kustomizeBuildDirective) Name() string { | ||
return "kustomize-build" | ||
} | ||
|
||
// Run implements the Directive interface. | ||
func (d *kustomizeBuildDirective) Run(_ context.Context, stepCtx *StepContext) (Result, error) { | ||
failure := Result{Status: StatusFailure} | ||
|
||
// Validate the configuration against the JSON Schema. | ||
if err := validate(d.schemaLoader, gojsonschema.NewGoLoader(stepCtx.Config), d.Name()); err != nil { | ||
return failure, err | ||
} | ||
|
||
// Convert the configuration into a typed object. | ||
cfg, err := configToStruct[KustomizeBuildConfig](stepCtx.Config) | ||
if err != nil { | ||
return failure, fmt.Errorf("could not convert config into %s config: %w", d.Name(), err) | ||
} | ||
|
||
return d.run(stepCtx, cfg) | ||
} | ||
|
||
func (d *kustomizeBuildDirective) run( | ||
stepCtx *StepContext, | ||
cfg KustomizeBuildConfig, | ||
) (_ Result, err error) { | ||
// Create a "chrooted" filesystem for the kustomize build. | ||
fs, err := securefs.MakeFsOnDiskSecureBuild(stepCtx.WorkDir) | ||
if err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
|
||
// Build the manifests. | ||
rm, err := kustomizeBuild(fs, filepath.Join(stepCtx.WorkDir, cfg.Path)) | ||
if err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
|
||
// Prepare the output path. | ||
outPath, err := securejoin.SecureJoin(stepCtx.WorkDir, cfg.OutPath) | ||
if err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
if err = os.MkdirAll(filepath.Dir(outPath), 0o700); err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
|
||
// Write the built manifests to the output path. | ||
b, err := rm.AsYaml() | ||
if err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
if err = os.WriteFile(outPath, b, 0o600); err != nil { | ||
return Result{Status: StatusFailure}, err | ||
} | ||
return Result{Status: StatusSuccess}, nil | ||
} | ||
|
||
// kustomizeBuild builds the manifests in the given directory using Kustomize. | ||
func kustomizeBuild(fs filesys.FileSystem, path string) (_ resmap.ResMap, err error) { | ||
kustomizeRenderMutex.Lock() | ||
defer kustomizeRenderMutex.Unlock() | ||
|
||
// Kustomize can panic in unpredicted ways due to (accidental) | ||
// invalid object data; recover when this happens to ensure | ||
// continuity of operations. | ||
defer func() { | ||
if r := recover(); r != nil { | ||
err = fmt.Errorf("recovered from kustomize build panic: %v", r) | ||
} | ||
}() | ||
|
||
buildOptions := &krusty.Options{ | ||
LoadRestrictions: kustypes.LoadRestrictionsNone, | ||
PluginConfig: kustypes.DisabledPluginConfig(), | ||
} | ||
|
||
k := krusty.MakeKustomizer(buildOptions) | ||
return k.Run(fs, path) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package directives | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Test_kustomizeBuildDirective_run(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
setupFiles func(*testing.T, string) | ||
config KustomizeBuildConfig | ||
assertions func(*testing.T, string, Result, error) | ||
}{ | ||
{ | ||
name: "successful build", | ||
setupFiles: func(t *testing.T, dir string) { | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(` | ||
apiVersion: kustomize.config.k8s.io/v1beta1 | ||
kind: Kustomization | ||
resources: | ||
- deployment.yaml | ||
`), 0o600)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "deployment.yaml"), []byte(`--- | ||
apiVersion: apps/v1 | ||
kind: Deployment | ||
metadata: | ||
name: test-deployment | ||
`), 0o600)) | ||
}, | ||
config: KustomizeBuildConfig{ | ||
Path: ".", | ||
OutPath: "output.yaml", | ||
}, | ||
assertions: func(t *testing.T, dir string, result Result, err error) { | ||
require.NoError(t, err) | ||
assert.Equal(t, Result{Status: StatusSuccess}, result) | ||
|
||
assert.FileExists(t, filepath.Join(dir, "output.yaml")) | ||
b, err := os.ReadFile(filepath.Join(dir, "output.yaml")) | ||
require.NoError(t, err) | ||
assert.Contains(t, string(b), "test-deployment") | ||
}, | ||
}, | ||
{ | ||
name: "kustomization file not found", | ||
setupFiles: func(*testing.T, string) {}, | ||
config: KustomizeBuildConfig{ | ||
Path: "invalid/", | ||
OutPath: "output.yaml", | ||
}, | ||
assertions: func(t *testing.T, dir string, result Result, err error) { | ||
require.ErrorContains(t, err, "no such file or directory") | ||
assert.Equal(t, Result{Status: StatusFailure}, result) | ||
|
||
assert.NoFileExists(t, filepath.Join(dir, "output.yaml")) | ||
}, | ||
}, | ||
{ | ||
name: "invalid kustomization", | ||
setupFiles: func(t *testing.T, dir string) { | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "kustomization.yaml"), []byte(`invalid`), 0o600)) | ||
}, | ||
config: KustomizeBuildConfig{ | ||
Path: ".", | ||
OutPath: "output.yaml", | ||
}, | ||
assertions: func(t *testing.T, dir string, result Result, err error) { | ||
require.ErrorContains(t, err, "invalid Kustomization") | ||
assert.Equal(t, Result{Status: StatusFailure}, result) | ||
|
||
assert.NoFileExists(t, filepath.Join(dir, "output.yaml")) | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
tempDir := t.TempDir() | ||
|
||
tt.setupFiles(t, tempDir) | ||
|
||
stepCtx := &StepContext{ | ||
WorkDir: tempDir, | ||
} | ||
|
||
d := &kustomizeBuildDirective{} | ||
result, err := d.run(stepCtx, tt.config) | ||
tt.assertions(t, tempDir, result, err) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"$schema": "https://json-schema.org/draft/2020-12/schema", | ||
"title": "KustomizeBuildConfig", | ||
"type": "object", | ||
"additionalProperties": false, | ||
"required": ["path", "outPath"], | ||
"properties": { | ||
"path": { | ||
"type": "string", | ||
"description": "Path to the directory containing the Kustomization file.", | ||
"minLength": 1 | ||
}, | ||
"outPath": { | ||
"type": "string", | ||
"description": "OutPath is the file path to write the built manifests to.", | ||
"minLength": 1 | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.