diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 0cebfd0dbed..e42236e0614 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -19,6 +19,7 @@ package main import ( "fmt" "os" + "os/signal" "github.com/spf13/cobra" @@ -69,13 +70,31 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return err } - manifests, err := builder.Build() - if err != nil { - return err + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + manifests, err := builder.Build() + if err != nil { + errChan <- err + } + + cmd.Print(string(manifests)) + errChan <- nil + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } } - cmd.Print(string(manifests)) - return nil } diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index 4a2afed567a..e4da62c872a 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -19,6 +19,7 @@ package main import ( "fmt" "os" + "os/signal" "github.com/spf13/cobra" @@ -67,13 +68,31 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { return err } - output, err := builder.Diff() - if err != nil { - return err + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + output, err := builder.Diff() + if err != nil { + errChan <- err + } + + cmd.Print(output) + errChan <- nil + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } } - cmd.Print(output) - return nil } diff --git a/go.mod b/go.mod index e3cb91ad249..bce21f8d58f 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/fluxcd/image-reflector-controller/api v0.15.0 github.com/fluxcd/kustomize-controller/api v0.19.1 github.com/fluxcd/notification-controller/api v0.20.1 - github.com/fluxcd/pkg/apis/kustomize v0.3.1 + github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect github.com/fluxcd/pkg/apis/meta v0.10.2 github.com/fluxcd/pkg/runtime v0.12.3 github.com/fluxcd/pkg/ssa v0.10.0 @@ -25,7 +25,7 @@ require ( github.com/gonvenience/ytbx v1.4.2 github.com/google/go-cmp v0.5.6 github.com/google/go-containerregistry v0.2.0 - github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/homeport/dyff v1.4.6 github.com/lucasb-eyer/go-colorful v1.2.0 @@ -49,7 +49,7 @@ require ( ) require ( - github.com/drone/envsubst v1.0.3 + github.com/fluxcd/pkg/kustomize v0.0.2 sigs.k8s.io/kustomize/kyaml v0.13.0 ) @@ -72,6 +72,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect diff --git a/go.sum b/go.sum index adaf1efb6fb..fdcea43cf5e 100644 --- a/go.sum +++ b/go.sum @@ -194,8 +194,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= -github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= +github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= @@ -240,6 +240,8 @@ github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVf github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc= github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= +github.com/fluxcd/pkg/kustomize v0.0.2 h1:ipvQrxSeuGZDsPZrVUL6tYMlTR5xqYTZp6G0Tdy2hVs= +github.com/fluxcd/pkg/kustomize v0.0.2/go.mod h1:AFwnf3OqQmpTCuwCARTGpPRMBf0ZFJNGCvW63KbgK04= github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= github.com/fluxcd/pkg/ssa v0.10.0 h1:dhgWDeqz0/zAs5guzmPx/DMPCkzZdlEiPvCs1NChAQM= diff --git a/internal/kustomization/build.go b/internal/kustomization/build.go index 1b17ed4123c..c8551cd6676 100644 --- a/internal/kustomization/build.go +++ b/internal/kustomization/build.go @@ -21,10 +21,12 @@ import ( "context" "encoding/base64" "fmt" + "sync" "time" "github.com/fluxcd/flux2/internal/utils" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/kustomize" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -37,9 +39,9 @@ import ( ) const ( - controllerName = "kustomize-controller" - controllerGroup = "kustomize.toolkit.fluxcd.io" - mask string = "**SOPS**" + controllerName = "kustomize-controller" + controllerGroup = "kustomize.toolkit.fluxcd.io" + mask = "**SOPS**" ) var defaultTimeout = 80 * time.Second @@ -53,6 +55,9 @@ type Builder struct { name string namespace string resourcesPath string + // mu is used to synchronize access to the kustomization file + mu sync.Mutex + action kustomize.Action kustomization *kustomizev1.Kustomization timeout time.Duration } @@ -149,13 +154,15 @@ func (b *Builder) build() (m resmap.ResMap, err error) { // generate kustomization.yaml if needed action, er := b.generate(*k, b.resourcesPath) if er != nil { - errf := CleanDirectory(b.resourcesPath, action) + errf := kustomize.CleanDirectory(b.resourcesPath, action) err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) return } + b.action = action + defer func() { - errf := CleanDirectory(b.resourcesPath, action) + errf := b.Cancel() if err == nil { err = errf } @@ -185,18 +192,28 @@ func (b *Builder) build() (m resmap.ResMap, err error) { } -func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (action, error) { +func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil { return "", err } - gen := NewGenerator(unstructured.Unstructured{Object: data}) - return gen.WriteFile(dirPath, WithSaveOriginalKustomization()) + gen := kustomize.NewGenerator(unstructured.Unstructured{Object: data}) + + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) } func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { fs := filesys.MakeFsOnDisk() - m, err := BuildKustomization(fs, dirPath) + + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + m, err := kustomize.BuildKustomization(fs, dirPath) if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) } @@ -208,7 +225,7 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio if err != nil { return nil, err } - outRes, err := SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res) + outRes, err := kustomize.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) } @@ -263,3 +280,18 @@ func trimSopsData(res *resource.Resource) error { return nil } + +// Cancel cancels the build +// It restores a clean reprository +func (b *Builder) Cancel() error { + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + err := kustomize.CleanDirectory(b.resourcesPath, b.action) + if err != nil { + return err + } + + return nil +} diff --git a/internal/kustomization/diff.go b/internal/kustomization/diff.go index f084b56ba2d..832d1c68fac 100644 --- a/internal/kustomization/diff.go +++ b/internal/kustomization/diff.go @@ -27,7 +27,7 @@ import ( ) func (b *Builder) Manager() (*ssa.ResourceManager, error) { - statusPoller := polling.NewStatusPoller(b.client, b.restMapper) + statusPoller := polling.NewStatusPoller(b.client, b.restMapper, nil) owner := ssa.Owner{ Field: controllerName, Group: controllerGroup, @@ -53,8 +53,6 @@ func (b *Builder) Diff() (string, error) { return "", err } - resourceManager.SetOwnerLabels(objects, b.kustomization.GetName(), b.kustomization.GetNamespace()) - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() diff --git a/internal/kustomization/kustomization_generator.go b/internal/kustomization/kustomization_generator.go deleted file mode 100644 index fc70fb8de4a..00000000000 --- a/internal/kustomization/kustomization_generator.go +++ /dev/null @@ -1,532 +0,0 @@ -/* -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. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -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" - "sigs.k8s.io/kustomize/api/resmap" - kustypes "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kyaml/filesys" - "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 unstructured.Unstructured -} - -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 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 { - errf := CleanDirectory(dirPath, action) - return action, fmt.Errorf("%w %s", err, errf) - } - - kus := kustypes.Kustomization{ - TypeMeta: kustypes.TypeMeta{ - APIVersion: kustypes.KustomizationVersion, - Kind: kustypes.KustomizationKind, - }, - } - - if err := yaml.Unmarshal(data, &kus); err != nil { - errf := CleanDirectory(dirPath, action) - return action, fmt.Errorf("%v %v", err, errf) - } - - 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 - } - - 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: p.Patch, - Target: adaptSelector(&p.Target), - }) - } - - 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 _, 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 { - errf := CleanDirectory(dirPath, action) - return action, fmt.Errorf("%v %v", err, errf) - } - kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{ - Patch: string(patch), - Target: adaptSelector(&p.Target), - }) - } - - 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, - NewTag: image.NewTag, - } - if exists, index := checkKustomizeImageExists(kus.Images, image.Name); exists { - kus.Images[index] = newImage - } else { - kus.Images = append(kus.Images, newImage) - } - } - - 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 - } - - 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 - -} - -func checkKustomizeImageExists(images []kustypes.Image, imageName string) (bool, int) { - for i, image := range images { - if imageName == image.Name { - return true, i - } - } - - return false, -1 -} - -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 unchangedAction, nil - } - } - - scan := func(base string) ([]string, error) { - var paths []string - pvd := provider.NewDefaultDepProvider() - rf := pvd.GetResourceFactory() - err := fs.Walk(base, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if path == base { - return nil - } - if info.IsDir() { - // If a sub-directory contains an existing kustomization file add the - // directory as a resource and do not decend into it. - for _, kfilename := range konfig.RecognizedKustomizationFileNames() { - if kpath := filepath.Join(path, kfilename); fs.Exists(kpath) && !fs.IsDir(kpath) { - paths = append(paths, path) - return filepath.SkipDir - } - } - return nil - } - - extension := filepath.Ext(path) - if extension != ".yaml" && extension != ".yml" { - return nil - } - - fContents, err := fs.ReadFile(path) - if err != nil { - return err - } - - if _, err := rf.SliceFromBytes(fContents); err != nil { - return fmt.Errorf("failed to decode Kubernetes YAML from %s: %w", path, err) - } - paths = append(paths, path) - return nil - }) - return paths, err - } - - abs, err := filepath.Abs(dirPath) - if err != nil { - return unchangedAction, err - } - - files, err := scan(abs) - if err != nil { - return unchangedAction, err - } - - kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName()) - f, err := fs.Create(kfile) - if err != nil { - return unchangedAction, err - } - f.Close() - - kus := kustypes.Kustomization{ - TypeMeta: kustypes.TypeMeta{ - APIVersion: kustypes.KustomizationVersion, - Kind: kustypes.KustomizationKind, - }, - } - - var resources []string - for _, file := range files { - resources = append(resources, strings.Replace(file, abs, ".", 1)) - } - - kus.Resources = resources - kd, err := yaml.Marshal(kus) - if err != nil { - // delete the kustomization file - errf := CleanDirectory(dirPath, createdAction) - return unchangedAction, fmt.Errorf("%v %v", err, errf) - } - - return createdAction, os.WriteFile(kfile, kd, os.ModePerm) -} - -func adaptSelector(selector *kustomize.Selector) (output *kustypes.Selector) { - if selector != nil { - output = &kustypes.Selector{} - output.Gvk.Group = selector.Group - output.Gvk.Kind = selector.Kind - output.Gvk.Version = selector.Version - output.Name = selector.Name - output.Namespace = selector.Namespace - output.LabelSelector = selector.LabelSelector - output.AnnotationSelector = selector.AnnotationSelector - } - return -} - -// TODO: remove mutex when kustomize fixes the concurrent map read/write panic -var kustomizeBuildMutex sync.Mutex - -// 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) { - // temporary workaround for concurrent map read and map write bug - // https://github.com/kubernetes-sigs/kustomize/issues/3659 - kustomizeBuildMutex.Lock() - defer kustomizeBuildMutex.Unlock() - - buildOptions := &krusty.Options{ - LoadRestrictions: kustypes.LoadRestrictionsNone, - PluginConfig: kustypes.DisabledPluginConfig(), - } - - 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 -} diff --git a/internal/kustomization/kustomization_varsub.go b/internal/kustomization/kustomization_varsub.go deleted file mode 100644 index f89e327316b..00000000000 --- a/internal/kustomization/kustomization_varsub.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright 2021 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. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kustomization - -import ( - "context" - "fmt" - "regexp" - "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" - "sigs.k8s.io/yaml" -) - -const ( - // varsubRegex is the regular expression used to validate - // the var names before substitution - varsubRegex = "^[_[:alpha:]][_[:alpha:][:digit:]]*$" - DisabledValue = "disabled" -) - -// 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 unstructured.Unstructured, - res *resource.Resource) (*resource.Resource, error) { - resData, err := res.AsYAML() - if err != nil { - return nil, err - } - - key := fmt.Sprintf("%s/substitute", kustomization.GetObjectKind().GroupVersionKind().Group) - - if res.GetLabels()[key] == DisabledValue || res.GetAnnotations()[key] == DisabledValue { - return nil, nil - } - - // load vars from ConfigMaps and Secrets data keys - 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": - resource := &corev1.ConfigMap{} - if err := kubeClient.Get(ctx, namespacedName, resource); err != nil { - return nil, fmt.Errorf("substitute from 'ConfigMap/%s' error: %w", reference.Name, err) - } - for k, v := range resource.Data { - vars[k] = strings.Replace(v, "\n", "", -1) - } - case "Secret": - resource := &corev1.Secret{} - if err := kubeClient.Get(ctx, namespacedName, resource); err != nil { - return nil, fmt.Errorf("substitute from 'Secret/%s' error: %w", reference.Name, err) - } - for k, v := range resource.Data { - vars[k] = strings.Replace(string(v), "\n", "", -1) - } - } - } - - 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 - -}