Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Prebuild Stage Feature #430

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
25 changes: 25 additions & 0 deletions api/v1beta1/kustomization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ type KustomizationSpec struct {
// +optional
Path string `json:"path,omitempty"`

// PreBuild describes which actions to perform on the YAML manifest
// before it is built by the kustomize overlay.
// +optional
PreBuild *PreBuild `json:"preBuild,omitempty"`

// PostBuild describes which actions to perform on the YAML manifest
// generated by building the kustomize overlay.
// +optional
Expand Down Expand Up @@ -191,6 +196,26 @@ type PostBuild struct {
SubstituteFrom []SubstituteReference `json:"substituteFrom,omitempty"`
}

// Prebuild describes which actions to perform on the YAML manifest
// generated by building the kustomize overlay.
type PreBuild struct {
// Substitute holds a map of key/value pairs.
// The variables defined in your YAML manifests
// that match any of the keys defined in the map
// will be substituted with the set value.
// Includes support for bash string replacement functions
// e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.
// +optional
Substitute map[string]string `json:"substitute,omitempty"`

// SubstituteFrom holds references to ConfigMaps and Secrets containing
// the variables and their values to be substituted in the YAML manifests.
// The ConfigMap and the Secret data keys represent the var names and they
// must match the vars declared in the manifests for the substitution to happen.
// +optional
SubstituteFrom []SubstituteReference `json:"substituteFrom,omitempty"`
}

// SubstituteReference contains a reference to a resource containing
// the variables name and value.
type SubstituteReference struct {
Expand Down
32 changes: 32 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,36 @@ spec:
type: object
type: array
type: object
preBuild:
description: PreBuild describes which actions to perform on the YAML manifest before it is built by the kustomize overlay.
properties:
substitute:
additionalProperties:
type: string
description: Substitute holds a map of key/value pairs. The variables defined in your YAML manifests that match any of the keys defined in the map will be substituted with the set value. Includes support for bash string replacement functions e.g. ${var:=default}, ${var:position} and ${var/substring/replacement}.
type: object
substituteFrom:
description: SubstituteFrom holds references to ConfigMaps and Secrets containing the variables and their values to be substituted in the YAML manifests. The ConfigMap and the Secret data keys represent the var names and they must match the vars declared in the manifests for the substitution to happen.
items:
description: SubstituteReference contains a reference to a resource containing the variables name and value.
properties:
kind:
description: Kind of the values referent, valid values are ('Secret', 'ConfigMap').
enum:
- Secret
- ConfigMap
type: string
name:
description: Name of the values referent. Should reside in the same namespace as the referring resource.
maxLength: 253
minLength: 1
type: string
required:
- kind
- name
type: object
type: array
type: object
prune:
description: Prune enables garbage collection.
type: boolean
Expand Down
3 changes: 2 additions & 1 deletion controllers/kustomization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ func (r *KustomizationReconciler) reconcile(
ctx context.Context,
kustomization kustomizev1.Kustomization,
source sourcev1.Source) (kustomizev1.Kustomization, error) {

// record the value of the reconciliation request, if any
if v, ok := meta.ReconcileAnnotationValue(kustomization.GetAnnotations()); ok {
kustomization.Status.SetLastHandledReconcileRequest(v)
Expand Down Expand Up @@ -500,7 +501,7 @@ func (r *KustomizationReconciler) getSource(ctx context.Context, kustomization k

func (r *KustomizationReconciler) generate(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization, dirPath string) (string, error) {
gen := NewGenerator(kustomization, kubeClient)
return gen.WriteFile(ctx, dirPath)
return gen.WriteFile(ctx, dirPath, kubeClient, kustomization)
}

func (r *KustomizationReconciler) build(ctx context.Context, kustomization kustomizev1.Kustomization, checksum, dirPath string) (*kustomizev1.Snapshot, error) {
Expand Down
50 changes: 41 additions & 9 deletions controllers/kustomization_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewGenerator(kustomization kustomizev1.Kustomization, kubeClient client.Cli
}
}

func (kg *KustomizeGenerator) WriteFile(ctx context.Context, dirPath string) (string, error) {
func (kg *KustomizeGenerator) WriteFile(ctx context.Context, dirPath string, kubeClient client.Client, kustomization kustomizev1.Kustomization) (string, error) {
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())

checksum, err := kg.checksum(ctx, dirPath)
Expand Down Expand Up @@ -140,6 +140,15 @@ func (kg *KustomizeGenerator) WriteFile(ctx context.Context, dirPath string) (st
}
}

// if prebuild is specified, substitude kustomization with given variables
// and return resulting kustomization
if kustomization.Spec.PreBuild != nil {
kus, err = preSubstituteVariables(ctx, kubeClient, kustomization, kus)
if err != nil {
return "", fmt.Errorf("var substitution failed for '%s'", err)
}
}

kd, err := yaml.Marshal(kus)
if err != nil {
return "", err
Expand All @@ -158,14 +167,14 @@ func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool,
return false, -1
}

func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
func (kg *KustomizeGenerator) generateKustomization(dirPath string) (string, error) {
fs := filesys.MakeFsOnDisk()

// Determine if there already is a Kustomization file at the root,
// as this means we do not have to generate one.
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
if kpath := filepath.Join(dirPath, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) {
return nil
return kpath, nil
}
}

Expand Down Expand Up @@ -213,18 +222,18 @@ func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {

abs, err := filepath.Abs(dirPath)
if err != nil {
return err
return "", err
}

files, err := scan(abs)
if err != nil {
return err
return "", err
}

kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
f, err := fs.Create(kfile)
if err != nil {
return err
return "", err
}
f.Close()

Expand All @@ -243,17 +252,40 @@ func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
kus.Resources = resources
kd, err := yaml.Marshal(kus)
if err != nil {
return err
return "", err
}

return ioutil.WriteFile(kfile, kd, os.ModePerm)
return kfile, ioutil.WriteFile(kfile, kd, os.ModePerm)
}

func (kg *KustomizeGenerator) checksum(ctx context.Context, dirPath string) (string, error) {
if err := kg.generateKustomization(dirPath); err != nil {
kf, err := kg.generateKustomization(dirPath)
if err != nil {
return "", fmt.Errorf("kustomize create failed: %w", err)
}

// Run PreBuild Variable Substitution
if kg.kustomization.Spec.PreBuild != nil {
content, err := ioutil.ReadFile(kf)
if err != nil {
return "", fmt.Errorf("kustomize prebuild failed: %w", err)
}

kd := kustypes.Kustomization{}
err = yaml.Unmarshal(content, &kd)
if err != nil {
return "", fmt.Errorf("kustomize prebuild failed: %w", err)
}

kd, err = preSubstituteVariables(ctx, kg.Client, kg.kustomization, kd)
if err != nil {
return "", fmt.Errorf("var substitution failed for '%s'", err)
}

kdYaml, err := yaml.Marshal(kd)
ioutil.WriteFile(kf, kdYaml, os.ModePerm)
}

fs := filesys.MakeFsOnDisk()
m, err := buildKustomization(fs, dirPath)
if err != nil {
Expand Down
108 changes: 89 additions & 19 deletions controllers/kustomization_varsub.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,31 @@ import (
"strings"

"github.com/drone/envsubst"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/resource"
kustypes "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/yaml"

kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
)

// varsubRegex is the regular expression used to validate
// the var names before substitution
const varsubRegex = "^[_[:alpha:]][_[:alpha:][:digit:]]*$"

// substituteVariables replaces the vars with their values in the specified resource.
// If a resource is labeled or annotated with
// 'kustomize.toolkit.fluxcd.io/substitute: disabled' the substitution is skipped.
func substituteVariables(
// substitudeFrom loads vars from configmaps and secrets referenced
// in the given SubstituteReference
func substitudeFrom(
ctx context.Context,
kubeClient client.Client,
kustomization kustomizev1.Kustomization,
res *resource.Resource) (*resource.Resource, error) {
resData, err := res.AsYAML()
if err != nil {
return nil, err
}

key := fmt.Sprintf("%s/substitute", kustomizev1.GroupVersion.Group)

if res.GetLabels()[key] == kustomizev1.DisabledValue || res.GetAnnotations()[key] == kustomizev1.DisabledValue {
return nil, nil
}

substituteReference []kustomizev1.SubstituteReference) (map[string]string, error) {
// placeholder variable for returnable substitutions
vars := make(map[string]string)

// load vars from ConfigMaps and Secrets data keys
for _, reference := range kustomization.Spec.PostBuild.SubstituteFrom {
for _, reference := range substituteReference {
namespacedName := types.NamespacedName{Namespace: kustomization.Namespace, Name: reference.Name}
switch reference.Kind {
case "ConfigMap":
Expand All @@ -63,6 +52,87 @@ func substituteVariables(
}
}
}
return vars, nil
}

// substituteVariables replaces the vars with their values in the specified resource.
// If a resource is labeled or annotated with
func preSubstituteVariables(
ctx context.Context,
kubeClient client.Client,
kustomization kustomizev1.Kustomization,
kustomizeFile kustypes.Kustomization) (kustypes.Kustomization, error) {
kd := kustypes.Kustomization{}
kustomizeData, err := yaml.Marshal(kustomizeFile)
if err != nil {
return kd, err
}

// load vars from ConfigMaps and Secrets data keys
vars, err := substitudeFrom(ctx, kubeClient, kustomization, kustomization.Spec.PreBuild.SubstituteFrom)
if err != nil {
return kd, err
}

// load in-line vars (overrides the ones from resources)
if kustomization.Spec.PreBuild.Substitute != nil {
for k, v := range kustomization.Spec.PreBuild.Substitute {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}

// run bash variable substitutions
r, _ := regexp.Compile(varsubRegex)
for v := range vars {
if !r.MatchString(v) {
return kd, fmt.Errorf("'%s' var name is invalid, must match '%s'", v, varsubRegex)
}
}

output, err := envsubst.Eval(string(kustomizeData), func(s string) string {
return vars[s]
})
if err != nil {
return kd, fmt.Errorf("variable substitution failed: %w", err)
}

json, err := yaml.YAMLToJSON([]byte(output))
if err != nil {
return kd, fmt.Errorf("Prebuild variable substitution failed: %w", err)
}

err = yaml.Unmarshal(json, &kd)
if err != nil {
return kd, err
}

return kd, nil
}

// substituteVariables replaces the vars with their values in the specified resource.
// If a resource is labeled or annotated with
// 'kustomize.toolkit.fluxcd.io/substitute: disabled' the substitution is skipped.
func substituteVariables(
ctx context.Context,
kubeClient client.Client,
kustomization kustomizev1.Kustomization,
res *resource.Resource) (*resource.Resource, error) {
resData, err := res.AsYAML()
if err != nil {
return nil, err
}

key := fmt.Sprintf("%s/substitute", kustomizev1.GroupVersion.Group)

if res.GetLabels()[key] == kustomizev1.DisabledValue || res.GetAnnotations()[key] == kustomizev1.DisabledValue {
return nil, nil
}

// load vars from ConfigMaps and Secrets data keys
vars, err := substitudeFrom(ctx, kubeClient, kustomization, kustomization.Spec.PostBuild.SubstituteFrom)
if err != nil {
return nil, err
}

// load in-line vars (overrides the ones from resources)
if kustomization.Spec.PostBuild.Substitute != nil {
Expand Down
Loading