1
0
mirror of synced 2026-02-06 19:05:55 +00:00

Add e2e tests for build/diff kustomization

Signed-off-by: Soule BA <soule@weave.works>
This commit is contained in:
Soule BA
2021-12-15 16:23:54 +01:00
parent 9376c9a946
commit f7d9ee90cd
35 changed files with 1485 additions and 224 deletions

View File

@@ -21,23 +21,26 @@ import (
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"time"
"github.com/fluxcd/flux2/internal/utils"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
const mask string = "**SOPS**"
const (
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
mask string = "**SOPS**"
)
var defaultTimeout = 80 * time.Second
@@ -65,17 +68,13 @@ func WithTimeout(timeout time.Duration) BuilderOptionFunc {
// NewBuilder returns a new Builder
// to dp : create functional options
func NewBuilder(kubeconfig string, kubecontext string, namespace, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) {
kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
func NewBuilder(rcg *genericclioptions.ConfigFlags, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) {
kubeClient, err := utils.KubeClient(rcg)
if err != nil {
return nil, err
}
cfg, err := utils.KubeConfig(kubeconfig, kubecontext)
if err != nil {
return nil, err
}
restMapper, err := apiutil.NewDynamicRESTMapper(cfg)
restMapper, err := rcg.ToRESTMapper()
if err != nil {
return nil, err
}
@@ -84,7 +83,7 @@ func NewBuilder(kubeconfig string, kubecontext string, namespace, name, resource
client: kubeClient,
restMapper: restMapper,
name: name,
namespace: namespace,
namespace: *rcg.Namespace,
resourcesPath: resources,
}
@@ -134,57 +133,70 @@ func (b *Builder) Build() ([]byte, error) {
return resources, nil
}
func (b *Builder) build() (resmap.ResMap, error) {
func (b *Builder) build() (m resmap.ResMap, err error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
// Get the kustomization object
k, err := b.getKustomization(ctx)
if err != nil {
return nil, err
}
// generate kustomization.yaml if needed
saved, err := b.generate(*k, b.resourcesPath)
if err != nil {
return nil, fmt.Errorf("failed to generate kustomization.yaml: %w", err)
}
// build the kustomization
m, err := b.do(ctx, *k, b.resourcesPath)
if err != nil {
return nil, err
}
// make sure secrets are masked
for _, res := range m.Resources() {
err := trimSopsData(res)
if err != nil {
return nil, err
}
return
}
// store the kustomization object
b.kustomization = k
// overwrite the kustomization.yaml to make sure it's clean
err = overwrite(saved, b.resourcesPath)
if err != nil {
return nil, fmt.Errorf("failed to restore kustomization.yaml: %w", err)
// generate kustomization.yaml if needed
action, er := b.generate(*k, b.resourcesPath)
if er != nil {
errf := CleanDirectory(b.resourcesPath, action)
err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf))
return
}
return m, nil
defer func() {
errf := CleanDirectory(b.resourcesPath, action)
if err == nil {
err = errf
}
}()
// build the kustomization
m, err = b.do(ctx, *k, b.resourcesPath)
if err != nil {
return
}
for _, res := range m.Resources() {
// set owner labels
err = b.setOwnerLabels(res)
if err != nil {
return
}
// make sure secrets are masked
err = trimSopsData(res)
if err != nil {
return
}
}
return
}
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) ([]byte, error) {
gen := NewGenerator(&kustomizeImpl{kustomization})
return gen.WriteFile(dirPath)
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (action, error) {
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
if err != nil {
return "", err
}
gen := NewGenerator(unstructured.Unstructured{Object: data})
return gen.WriteFile(dirPath, WithSaveOriginalKustomization())
}
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) {
fs := filesys.MakeFsOnDisk()
m, err := buildKustomization(fs, dirPath)
m, err := BuildKustomization(fs, dirPath)
if err != nil {
return nil, fmt.Errorf("kustomize build failed: %w", err)
}
@@ -192,7 +204,11 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio
for _, res := range m.Resources() {
// run variable substitutions
if kustomization.Spec.PostBuild != nil {
outRes, err := substituteVariables(ctx, b.client, &kustomizeImpl{kustomization}, res)
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
if err != nil {
return nil, err
}
outRes, err := SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res)
if err != nil {
return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err)
}
@@ -209,6 +225,20 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio
return m, nil
}
func (b *Builder) setOwnerLabels(res *resource.Resource) error {
labels := res.GetLabels()
labels[controllerGroup+"/name"] = b.kustomization.GetName()
labels[controllerGroup+"/namespace"] = b.kustomization.GetNamespace()
err := res.SetLabels(labels)
if err != nil {
return err
}
return nil
}
func trimSopsData(res *resource.Resource) error {
// sopsMess is the base64 encoded mask
sopsMess := base64.StdEncoding.EncodeToString([]byte(mask))
@@ -233,12 +263,3 @@ func trimSopsData(res *resource.Resource) error {
return nil
}
func overwrite(saved []byte, dirPath string) error {
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
err := os.WriteFile(kfile, saved, 0644)
if err != nil {
return fmt.Errorf("failed to overwrite kustomization.yaml: %w", err)
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"path/filepath"
"sort"
@@ -17,6 +18,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/homeport/dyff/pkg/dyff"
"github.com/lucasb-eyer/go-colorful"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
@@ -24,12 +26,7 @@ import (
"sigs.k8s.io/yaml"
)
const (
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
)
func (b *Builder) manager() (*ssa.ResourceManager, error) {
func (b *Builder) Manager() (*ssa.ResourceManager, error) {
statusPoller := polling.NewStatusPoller(b.client, b.restMapper)
owner := ssa.Owner{
Field: controllerName,
@@ -39,20 +36,21 @@ func (b *Builder) manager() (*ssa.ResourceManager, error) {
return ssa.NewResourceManager(b.client, statusPoller, owner), nil
}
func (b *Builder) Diff() error {
func (b *Builder) Diff() (string, error) {
output := strings.Builder{}
res, err := b.Build()
if err != nil {
return err
return "", err
}
// convert the build result into Kubernetes unstructured objects
objects, err := ssa.ReadObjects(bytes.NewReader(res))
if err != nil {
return err
return "", err
}
resourceManager, err := b.manager()
resourceManager, err := b.Manager()
if err != nil {
return err
return "", err
}
resourceManager.SetOwnerLabels(objects, b.kustomization.GetName(), b.kustomization.GetNamespace())
@@ -61,7 +59,7 @@ func (b *Builder) Diff() error {
defer cancel()
if err := ssa.SetNativeKindsDefaults(objects); err != nil {
return err
return "", err
}
// create an inventory of objects to be reconciled
@@ -69,10 +67,10 @@ func (b *Builder) Diff() error {
for _, obj := range objects {
change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj)
if err != nil {
if b.kustomization.Spec.Force && strings.Contains(err.Error(), "immutable") {
writeString(fmt.Sprintf("► %s created", obj.GetName()), bunt.Green)
if b.kustomization.Spec.Force && isImmutableError(err) {
output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green))
} else {
writeString(fmt.Sprint(``, err), bunt.Red)
output.WriteString(writeString(fmt.Sprint(``, err), bunt.Red))
}
continue
}
@@ -84,20 +82,20 @@ func (b *Builder) Diff() error {
}
if change.Action == string(ssa.CreatedAction) {
writeString(fmt.Sprintf("► %s created", change.Subject), bunt.Green)
output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green))
}
if change.Action == string(ssa.ConfiguredAction) {
writeString(fmt.Sprintf("► %s drifted", change.Subject), bunt.WhiteSmoke)
output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke))
liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject)
if err != nil {
return err
return "", err
}
defer cleanupDir(tmpDir)
err = diff(liveFile, mergedFile)
err = diff(liveFile, mergedFile, &output)
if err != nil {
return err
return "", err
}
}
@@ -109,15 +107,15 @@ func (b *Builder) Diff() error {
if oldStatus.Inventory != nil {
diffObjects, err := diffInventory(oldStatus.Inventory, newInventory)
if err != nil {
return err
return "", err
}
for _, object := range diffObjects {
writeString(fmt.Sprintf("► %s deleted", ssa.FmtUnstructured(object)), bunt.OrangeRed)
output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed))
}
}
}
return nil
return output.String(), nil
}
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {
@@ -141,19 +139,19 @@ func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, st
return liveFile, mergedFile, tmpDir, nil
}
func writeString(t string, color colorful.Color) {
fmt.Println(bunt.Style(
func writeString(t string, color colorful.Color) string {
return bunt.Style(
t,
bunt.EachLine(),
bunt.Foreground(color),
))
)
}
func cleanupDir(dir string) error {
return os.RemoveAll(dir)
}
func diff(liveFile, mergedFile string) error {
func diff(liveFile, mergedFile string, output io.Writer) error {
from, to, err := ytbx.LoadFiles(liveFile, mergedFile)
if err != nil {
return fmt.Errorf("failed to load input files: %w", err)
@@ -172,7 +170,7 @@ func diff(liveFile, mergedFile string) error {
OmitHeader: true,
}
if err := reportWriter.WriteReport(os.Stdout); err != nil {
if err := reportWriter.WriteReport(output); err != nil {
return fmt.Errorf("failed to print report: %w", err)
}
@@ -285,3 +283,12 @@ func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.Change
return nil
}
func isImmutableError(err error) bool {
// Detect immutability like kubectl does
// https://github.com/kubernetes/kubectl/blob/8165f83007/pkg/cmd/apply/patcher.go#L201
if errors.IsConflict(err) || errors.IsInvalid(err) {
return true
}
return false
}

View File

@@ -16,69 +16,9 @@ limitations under the License.
package kustomization
import (
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
"github.com/fluxcd/pkg/apis/kustomize"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Kustomize defines the methods to retrieve the kustomization information
// TO DO @souleb: move this to fluxcd/pkg along with generator and varsub
type Kustomize interface {
client.Object
GetTargetNamespace() string
GetPatches() []kustomize.Patch
GetPatchesStrategicMerge() []apiextensionsv1.JSON
GetPatchesJSON6902() []kustomize.JSON6902Patch
GetImages() []kustomize.Image
GetSubstituteFrom() []SubstituteReference
GetSubstitute() map[string]string
}
// SubstituteReference contains a reference to a resource containing
// the variables name and value.
type SubstituteReference struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
// TO DO @souleb: this is a temporary hack to get the kustomize object
// from the kustomize controller.
// At some point we should remove this and have the kustomize controller implement
// the Kustomize interface.
type kustomizeImpl struct {
kustomizev1.Kustomization
}
func (k *kustomizeImpl) GetTargetNamespace() string {
return k.Spec.TargetNamespace
}
func (k *kustomizeImpl) GetPatches() []kustomize.Patch {
return k.Spec.Patches
}
func (k *kustomizeImpl) GetPatchesStrategicMerge() []apiextensionsv1.JSON {
return k.Spec.PatchesStrategicMerge
}
func (k *kustomizeImpl) GetPatchesJSON6902() []kustomize.JSON6902Patch {
return k.Spec.PatchesJSON6902
}
func (k *kustomizeImpl) GetImages() []kustomize.Image {
return k.Spec.Images
}
func (k *kustomizeImpl) GetSubstituteFrom() []SubstituteReference {
refs := make([]SubstituteReference, 0, len(k.Spec.PostBuild.SubstituteFrom))
for _, s := range k.Spec.PostBuild.SubstituteFrom {
refs = append(refs, SubstituteReference(s))
}
return refs
}
func (k *kustomizeImpl) GetSubstitute() map[string]string {
return k.Spec.PostBuild.Substitute
}

View File

@@ -1,5 +1,5 @@
/*
Copyright 2021 The Flux authors
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,11 +19,15 @@ package kustomization
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/krusty"
"sigs.k8s.io/kustomize/api/provider"
@@ -33,32 +37,74 @@ import (
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/kustomize"
"github.com/hashicorp/go-multierror"
)
const (
specField = "spec"
targetNSField = "targetNamespace"
patchesField = "patches"
patchesSMField = "patchesStrategicMerge"
patchesJson6902Field = "patchesJson6902"
imagesField = "images"
originalKustomizationFile = "kustomization.yaml.original"
)
type action string
const (
createdAction action = "created"
unchangedAction action = "unchanged"
)
type KustomizeGenerator struct {
kustomization Kustomize
kustomization unstructured.Unstructured
}
func NewGenerator(kustomization Kustomize) *KustomizeGenerator {
type SavingOptions func(dirPath, file string, action action) error
func NewGenerator(kustomization unstructured.Unstructured) *KustomizeGenerator {
return &KustomizeGenerator{
kustomization: kustomization,
}
}
func WithSaveOriginalKustomization() SavingOptions {
return func(dirPath, kfile string, action action) error {
// copy the original kustomization.yaml to the directory if we did not create it
if action != createdAction {
if err := copyFile(kfile, filepath.Join(dirPath, originalKustomizationFile)); err != nil {
errf := CleanDirectory(dirPath, action)
return fmt.Errorf("%v %v", err, errf)
}
}
return nil
}
}
// WriteFile generates a kustomization.yaml in the given directory if it does not exist.
// It apply the flux kustomize resources to the kustomization.yaml and then write the
// updated kustomization.yaml to the directory.
// It returns the original kustomization.yaml.
func (kg *KustomizeGenerator) WriteFile(dirPath string) ([]byte, error) {
if err := kg.generateKustomization(dirPath); err != nil {
return nil, err
// It returns an action that indicates if the kustomization.yaml was created or not.
// It is the caller responsability to clean up the directory by use the provided function CleanDirectory.
// example:
// err := CleanDirectory(dirPath, action)
// if err != nil {
// log.Fatal(err)
// }
func (kg *KustomizeGenerator) WriteFile(dirPath string, opts ...SavingOptions) (action, error) {
action, err := kg.generateKustomization(dirPath)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
data, err := os.ReadFile(kfile)
if err != nil {
return nil, err
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%w %s", err, errf)
}
kus := kustypes.Kustomization{
@@ -69,36 +115,67 @@ func (kg *KustomizeGenerator) WriteFile(dirPath string) ([]byte, error) {
}
if err := yaml.Unmarshal(data, &kus); err != nil {
return nil, err
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
if kg.kustomization.GetTargetNamespace() != "" {
kus.Namespace = kg.kustomization.GetTargetNamespace()
tg, ok, err := kg.getNestedString(specField, targetNSField)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
if ok {
kus.Namespace = tg
}
for _, m := range kg.kustomization.GetPatches() {
patches, err := kg.getPatches()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patches: %w", fmt.Errorf("%v %v", err, errf))
}
for _, p := range patches {
kus.Patches = append(kus.Patches, kustypes.Patch{
Patch: m.Patch,
Target: adaptSelector(&m.Target),
Patch: p.Patch,
Target: adaptSelector(&p.Target),
})
}
for _, m := range kg.kustomization.GetPatchesStrategicMerge() {
kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(m.Raw))
patchesSM, err := kg.getPatchesStrategicMerge()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patchesStrategicMerge: %w", fmt.Errorf("%v %v", err, errf))
}
for _, m := range kg.kustomization.GetPatchesJSON6902() {
patch, err := json.Marshal(m.Patch)
for _, p := range patchesSM {
kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(p.Raw))
}
patchesJSON, err := kg.getPatchesJson6902()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get patchesJson6902: %w", fmt.Errorf("%v %v", err, errf))
}
for _, p := range patchesJSON {
patch, err := json.Marshal(p.Patch)
if err != nil {
return nil, err
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{
Patch: string(patch),
Target: adaptSelector(&m.Target),
Target: adaptSelector(&p.Target),
})
}
for _, image := range kg.kustomization.GetImages() {
images, err := kg.getImages()
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("unable to get images: %w", fmt.Errorf("%v %v", err, errf))
}
for _, image := range images {
newImage := kustypes.Image{
Name: image.Name,
NewName: image.NewName,
@@ -112,13 +189,141 @@ func (kg *KustomizeGenerator) WriteFile(dirPath string) ([]byte, error) {
}
manifest, err := yaml.Marshal(kus)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
// copy the original kustomization.yaml to the directory if we did not create it
for _, opt := range opts {
if err := opt(dirPath, kfile, action); err != nil {
return action, fmt.Errorf("failed to save original kustomization.yaml: %w", err)
}
}
err = os.WriteFile(kfile, manifest, os.ModePerm)
if err != nil {
errf := CleanDirectory(dirPath, action)
return action, fmt.Errorf("%v %v", err, errf)
}
return action, nil
}
func (kg *KustomizeGenerator) getPatches() ([]kustomize.Patch, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesField)
if err != nil {
return nil, err
}
os.WriteFile(kfile, manifest, 0644)
var resultErr error
if ok {
res := make([]kustomize.Patch, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch kustomize.Patch
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getPatchesStrategicMerge() ([]apiextensionsv1.JSON, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesSMField)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]apiextensionsv1.JSON, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch apiextensionsv1.JSON
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getPatchesJson6902() ([]kustomize.JSON6902Patch, error) {
patches, ok, err := kg.getNestedSlice(specField, patchesJson6902Field)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]kustomize.JSON6902Patch, 0, len(patches))
for k, p := range patches {
patch, ok := p.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var kpatch kustomize.JSON6902Patch
err = runtime.DefaultUnstructuredConverter.FromUnstructured(patch, &kpatch)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, kpatch)
}
return res, resultErr
}
return nil, resultErr
}
func (kg *KustomizeGenerator) getImages() ([]kustomize.Image, error) {
img, ok, err := kg.getNestedSlice(specField, imagesField)
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]kustomize.Image, 0, len(img))
for k, i := range img {
im, ok := i.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var image kustomize.Image
err = runtime.DefaultUnstructuredConverter.FromUnstructured(im, &image)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, image)
}
return res, resultErr
}
return nil, resultErr
return data, nil
}
func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool, int) {
@@ -131,14 +336,32 @@ func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool,
return false, -1
}
func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
func (kg *KustomizeGenerator) getNestedString(fields ...string) (string, bool, error) {
val, ok, err := unstructured.NestedString(kg.kustomization.Object, fields...)
if err != nil {
return "", ok, err
}
return val, ok, nil
}
func (kg *KustomizeGenerator) getNestedSlice(fields ...string) ([]interface{}, bool, error) {
val, ok, err := unstructured.NestedSlice(kg.kustomization.Object, fields...)
if err != nil {
return nil, ok, err
}
return val, ok, nil
}
func (kg *KustomizeGenerator) generateKustomization(dirPath string) (action, 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 unchangedAction, nil
}
}
@@ -186,18 +409,18 @@ func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
abs, err := filepath.Abs(dirPath)
if err != nil {
return err
return unchangedAction, err
}
files, err := scan(abs)
if err != nil {
return err
return unchangedAction, err
}
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
f, err := fs.Create(kfile)
if err != nil {
return err
return unchangedAction, err
}
f.Close()
@@ -216,10 +439,12 @@ func (kg *KustomizeGenerator) generateKustomization(dirPath string) error {
kus.Resources = resources
kd, err := yaml.Marshal(kus)
if err != nil {
return err
// delete the kustomization file
errf := CleanDirectory(dirPath, createdAction)
return unchangedAction, fmt.Errorf("%v %v", err, errf)
}
return os.WriteFile(kfile, kd, os.ModePerm)
return createdAction, os.WriteFile(kfile, kd, os.ModePerm)
}
func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) {
@@ -239,10 +464,10 @@ func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) {
// TODO: remove mutex when kustomize fixes the concurrent map read/write panic
var kustomizeBuildMutex sync.Mutex
// buildKustomization wraps krusty.MakeKustomizer with the following settings:
// BuildKustomization wraps krusty.MakeKustomizer with the following settings:
// - load files from outside the kustomization.yaml root
// - disable plugins except for the builtin ones
func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) {
func BuildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) {
// temporary workaround for concurrent map read and map write bug
// https://github.com/kubernetes-sigs/kustomize/issues/3659
kustomizeBuildMutex.Lock()
@@ -256,3 +481,52 @@ func buildKustomization(fs filesys.FileSystem, dirPath string) (resmap.ResMap, e
k := krusty.MakeKustomizer(buildOptions)
return k.Run(fs, dirPath)
}
// CleanDirectory removes the kustomization.yaml file from the given directory.
func CleanDirectory(dirPath string, action action) error {
kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName())
originalFile := filepath.Join(dirPath, originalKustomizationFile)
// restore old file if it exists
if _, err := os.Stat(originalFile); err == nil {
err := os.Rename(originalFile, kfile)
if err != nil {
return fmt.Errorf("failed to cleanup repository: %w", err)
}
}
if action == createdAction {
return os.Remove(kfile)
}
return nil
}
// copyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist or else trucnated.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
errf := out.Close()
if err == nil {
err = errf
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
return
}

View File

@@ -23,7 +23,10 @@ import (
"strings"
"github.com/drone/envsubst"
"github.com/hashicorp/go-multierror"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/kustomize/api/resource"
@@ -37,13 +40,13 @@ const (
DisabledValue = "disabled"
)
// substituteVariables replaces the vars with their values in the specified resource.
// 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(
func SubstituteVariables(
ctx context.Context,
kubeClient client.Client,
kustomization Kustomize,
kustomization unstructured.Unstructured,
res *resource.Resource) (*resource.Resource, error) {
resData, err := res.AsYAML()
if err != nil {
@@ -56,10 +59,46 @@ func substituteVariables(
return nil, nil
}
vars := make(map[string]string)
// load vars from ConfigMaps and Secrets data keys
for _, reference := range kustomization.GetSubstituteFrom() {
vars, err := loadVars(ctx, kubeClient, kustomization)
if err != nil {
return nil, err
}
// load in-line vars (overrides the ones from resources)
substitute, ok, err := unstructured.NestedStringMap(kustomization.Object, "spec", "postBuild", "substitute")
if err != nil {
return nil, err
}
if ok {
for k, v := range substitute {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}
// run bash variable substitutions
if len(vars) > 0 {
jsonData, err := varSubstitution(resData, vars)
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}
err = res.UnmarshalJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
}
}
return res, nil
}
func loadVars(ctx context.Context, kubeClient client.Client, kustomization unstructured.Unstructured) (map[string]string, error) {
vars := make(map[string]string)
substituteFrom, err := getSubstituteFrom(kustomization)
if err != nil {
return nil, fmt.Errorf("unable to get subsituteFrom: %w", err)
}
for _, reference := range substituteFrom {
namespacedName := types.NamespacedName{Namespace: kustomization.GetNamespace(), Name: reference.Name}
switch reference.Kind {
case "ConfigMap":
@@ -81,39 +120,57 @@ func substituteVariables(
}
}
// load in-line vars (overrides the ones from resources)
if kustomization.GetSubstitute() != nil {
for k, v := range kustomization.GetSubstitute() {
vars[k] = strings.Replace(v, "\n", "", -1)
}
}
// run bash variable substitutions
if len(vars) > 0 {
r, _ := regexp.Compile(varsubRegex)
for v := range vars {
if !r.MatchString(v) {
return nil, fmt.Errorf("'%s' var name is invalid, must match '%s'", v, varsubRegex)
}
}
output, err := envsubst.Eval(string(resData), func(s string) string {
return vars[s]
})
if err != nil {
return nil, fmt.Errorf("variable substitution failed: %w", err)
}
jsonData, err := yaml.YAMLToJSON([]byte(output))
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}
err = res.UnmarshalJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("UnmarshalJSON: %w", err)
}
}
return res, nil
return vars, nil
}
func varSubstitution(data []byte, vars map[string]string) ([]byte, error) {
r, _ := regexp.Compile(varsubRegex)
for v := range vars {
if !r.MatchString(v) {
return nil, fmt.Errorf("'%s' var name is invalid, must match '%s'", v, varsubRegex)
}
}
output, err := envsubst.Eval(string(data), func(s string) string {
return vars[s]
})
if err != nil {
return nil, fmt.Errorf("variable substitution failed: %w", err)
}
jsonData, err := yaml.YAMLToJSON([]byte(output))
if err != nil {
return nil, fmt.Errorf("YAMLToJSON: %w", err)
}
return jsonData, nil
}
func getSubstituteFrom(kustomization unstructured.Unstructured) ([]SubstituteReference, error) {
substituteFrom, ok, err := unstructured.NestedSlice(kustomization.Object, "spec", "postBuild", "substituteFrom")
if err != nil {
return nil, err
}
var resultErr error
if ok {
res := make([]SubstituteReference, 0, len(substituteFrom))
for k, s := range substituteFrom {
sub, ok := s.(map[string]interface{})
if !ok {
err := fmt.Errorf("unable to convert patch %d to map[string]interface{}", k)
resultErr = multierror.Append(resultErr, err)
}
var substitute SubstituteReference
err = runtime.DefaultUnstructuredConverter.FromUnstructured(sub, &substitute)
if err != nil {
resultErr = multierror.Append(resultErr, err)
}
res = append(res, substitute)
}
return res, nil
}
return nil, resultErr
}