From 70fb87bc9378cdc9314235a87a1fbcefb0384df0 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Sat, 27 Nov 2021 17:53:30 +0100 Subject: [PATCH 1/4] Add a build kustomization feature If implemented it will permit queriying the Kubernetes API to fetch the specified Flux Kustomization, then uses the specified path to build the overlay. Signed-off-by: Soule BA --- cmd/flux/build.go | 31 +++ cmd/flux/build_kustomization.go | 80 ++++++ go.mod | 1 + go.sum | 5 +- internal/kustomization/build.go | 228 ++++++++++++++++ internal/kustomization/build_test.go | 120 ++++++++ internal/kustomization/kustomization.go | 84 ++++++ .../kustomization/kustomization_generator.go | 258 ++++++++++++++++++ .../kustomization/kustomization_varsub.go | 119 ++++++++ 9 files changed, 925 insertions(+), 1 deletion(-) create mode 100644 cmd/flux/build.go create mode 100644 cmd/flux/build_kustomization.go create mode 100644 internal/kustomization/build.go create mode 100644 internal/kustomization/build_test.go create mode 100644 internal/kustomization/kustomization.go create mode 100644 internal/kustomization/kustomization_generator.go create mode 100644 internal/kustomization/kustomization_varsub.go diff --git a/cmd/flux/build.go b/cmd/flux/build.go new file mode 100644 index 00000000..0c901036 --- /dev/null +++ b/cmd/flux/build.go @@ -0,0 +1,31 @@ +/* +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 main + +import ( + "github.com/spf13/cobra" +) + +var buildCmd = &cobra.Command{ + Use: "build", + Short: "Build a flux resource", + Long: "The build command is used to build flux resources.", +} + +func init() { + rootCmd.AddCommand(buildCmd) +} diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go new file mode 100644 index 00000000..d4d8a8a0 --- /dev/null +++ b/cmd/flux/build_kustomization.go @@ -0,0 +1,80 @@ +/* +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 main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/kustomization" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" +) + +var buildKsCmd = &cobra.Command{ + Use: "kustomization", + Aliases: []string{"ks"}, + Short: "Build Kustomization", + Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization, +then it uses the specified files or path to build the overlay to write the resulting multi-doc YAML to stdout.`, + Example: `# Create a new overlay. +flux build kustomization my-app --resources ./path/to/local/manifests`, + ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), + RunE: buildKsCmdRun, +} + +type buildKsFlags struct { + resources string +} + +var buildKsArgs buildKsFlags + +func init() { + buildKsCmd.Flags().StringVar(&buildKsArgs.resources, "resources", "", "Name of a file containing a file to add to the kustomization file.)") + buildCmd.AddCommand(buildKsCmd) +} + +func buildKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if buildKsArgs.resources == "" { + return fmt.Errorf("invalid resource path %q", buildKsArgs.resources) + } + + if fs, err := os.Stat(buildKsArgs.resources); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", buildKsArgs.resources) + } + + builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, buildKsArgs.resources, kustomization.WithTimeout(rootArgs.timeout)) + if err != nil { + return err + } + + manifests, err := builder.Build() + if err != nil { + return err + } + + cmd.Print(string(manifests)) + + return nil + +} diff --git a/go.mod b/go.mod index 4bfb0a14..86cb5883 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/go-git/go-git/v5 v5.4.2 github.com/google/go-cmp v0.5.6 github.com/google/go-containerregistry v0.2.0 + github.com/hashicorp/go-retryablehttp v0.7.0 // indirect github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-shellwords v1.0.12 github.com/olekukonko/tablewriter v0.0.4 diff --git a/go.sum b/go.sum index 17b4f820..dd909e60 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,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-0.20200804185402-58bc65f69603 h1:PMzPM0wCHDrXlO7TlEq5lqlhGKHEgSnUR4YMSEVKrQ0= +github.com/drone/envsubst v1.0.3-0.20200804185402-58bc65f69603/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= 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= @@ -449,8 +451,9 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= diff --git a/internal/kustomization/build.go b/internal/kustomization/build.go new file mode 100644 index 00000000..4b5279a4 --- /dev/null +++ b/internal/kustomization/build.go @@ -0,0 +1,228 @@ +/* +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 ( + "bytes" + "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/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "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**" + +var defaultTimeout = 80 * time.Second + +// Builder builds yaml manifests +// It retrieves the kustomization object from the k8s cluster +// and overlays the manifests with the resources specified in the resourcesPath +type Builder struct { + client client.WithWatch + name string + namespace string + resourcesPath string + kustomization *kustomizev1.Kustomization + timeout time.Duration +} + +type BuilderOptionFunc func(b *Builder) error + +func WithTimeout(timeout time.Duration) BuilderOptionFunc { + return func(b *Builder) error { + b.timeout = timeout + return nil + } +} + +// 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) + if err != nil { + return nil, err + } + + b := &Builder{ + client: kubeClient, + name: name, + namespace: namespace, + resourcesPath: resources, + } + + for _, opt := range opts { + if err := opt(b); err != nil { + return nil, err + } + } + + if b.timeout == 0 { + b.timeout = defaultTimeout + } + + return b, nil +} + +func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) { + namespacedName := types.NamespacedName{ + Namespace: b.namespace, + Name: b.name, + } + + k := &kustomizev1.Kustomization{} + err := b.client.Get(ctx, namespacedName, k) + if err != nil { + return nil, err + } + + return k, nil +} + +// Build builds the yaml manifests from the kustomization object +// and overlays the manifests with the resources specified in the resourcesPath +// It expects a kustomization.yaml file in the resourcesPath, and it will +// generate a kustomization.yaml file if it doesn't exist +func (b *Builder) Build() ([]byte, error) { + m, err := b.build() + if err != nil { + return nil, err + } + + resources, err := m.AsYaml() + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + return resources, nil +} + +func (b *Builder) build() (resmap.ResMap, 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 + } + } + + // store the kustomization object + b.kustomization = k + + // restore the kustomization.yaml + err = restore(saved, b.resourcesPath) + if err != nil { + return nil, fmt.Errorf("failed to restore kustomization.yaml: %w", err) + } + + return m, nil + +} + +func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) ([]byte, error) { + gen := NewGenerator(&kustomizeImpl{kustomization}) + return gen.WriteFile(dirPath) +} + +func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { + fs := filesys.MakeFsOnDisk() + m, err := buildKustomization(fs, dirPath) + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + for _, res := range m.Resources() { + // run variable substitutions + if kustomization.Spec.PostBuild != nil { + outRes, err := substituteVariables(ctx, b.client, &kustomizeImpl{kustomization}, res) + if err != nil { + return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) + } + + if outRes != nil { + _, err = m.Replace(res) + if err != nil { + return nil, err + } + } + } + } + + return m, nil +} + +func trimSopsData(res *resource.Resource) error { + // sopsMess is the base64 encoded mask + sopsMess := base64.StdEncoding.EncodeToString([]byte(mask)) + + if res.GetKind() == "Secret" { + dataMap := res.GetDataMap() + for k, v := range dataMap { + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + fmt.Println(fmt.Errorf("failed to decode secret data: %w", err)) + } + + if bytes.Contains(data, []byte("sops")) { + dataMap[k] = sopsMess + } + } + res.SetDataMap(dataMap) + } + + return nil +} + +func restore(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 restore kustomization.yaml: %w", err) + } + return nil +} diff --git a/internal/kustomization/build_test.go b/internal/kustomization/build_test.go new file mode 100644 index 00000000..7834e108 --- /dev/null +++ b/internal/kustomization/build_test.go @@ -0,0 +1,120 @@ +/* +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 ( + "testing" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestSanitizeResources(t *testing.T) { + testCases := []struct { + name string + yamlStr string + expected string + }{ + { + name: "secret with sops token", + yamlStr: `apiVersion: v1 +kind: Secret +metadata: + name: my-secret +type: Opaque +data: + token: | + ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpvQmU1UGxQbWZRQ1VVYzRzcUtJbW + p3PT0saXY6TUxMRVcxNVFDOWtSZFZWYWdKbnpMQ1NrMHhaR1dJcEFlVGZIenl4VDEwZz0s + dGFnOkszR2tCQ0dTK3V0NFRwazZuZGIwQ0E9PSx0eXBlOnN0cl0iLAoJInNvcHMiOiB7Cg + kJImttcyI6IG51bGwsCgkJImdjcF9rbXMiOiBudWxsLAoJCSJhenVyZV9rdiI6IG51bGws + CgkJImhjX3ZhdWx0IjogbnVsbCwKCQkiYWdlIjogWwoJCQl7CgkJCQkicmVjaXBpZW50Ij + ogImFnZTEwbGEyZ2Uwd3R2eDNxcjdkYXRxZjdyczR5bmd4c3pkYWw5MjdmczlydWthbXI4 + dTJwc2hzdnR6N2NlIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIE + ZJTEUtLS0tLVxuWVdkbExXVnVZM0o1Y0hScGIyNHViM0puTDNZeENpMCtJRmd5TlRVeE9T + QTFMMlJwWkhScksxRlNWbVlyZDFWYVxuWTBoeFdGUXpTREJzVDFrM1dqTnRZbVUxUW1saW + FESnljWGxOQ25GMVlqZE5PVGhWYlZOdk1HOXJOUzlaVVhad1xuTW5WMGJuUlVNR050ZWpG + UGJ6TTRVMlV6V2tzemVWa0tMUzB0SUdKNlVHaHhNVVYzWW1WSlRIbEpTVUpwUlZSWlxuVm + pkMFJWUmFkVTh3ZWt4WFRISXJZVXBsWWtOMmFFRUswSS9NQ0V0WFJrK2IvTjJHMUpGM3ZI + UVQyNGRTaFdZRFxudytKSVVTQTNhTGYyc3YwenIyTWRVRWRWV0JKb004blQ0RDR4VmJCT1 + JEKzY2OVcrOW5EZVN3PT1cbi0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG4i + CgkJCX0KCQldLAoJCSJsYXN0bW9kaWZpZWQiOiAiMjAyMS0xMS0yNlQxNjozNDo1MVoiLA + oJCSJtYWMiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpDT0d6ZjVZQ0hOTlA2ejRKYUVLcmpO + M004ZjUrUTF1S1VLVE1Id2ozODgvSUNtTHlpMnNTclRtajdQUCtYN005alRWd2E4d1ZnWV + RwTkxpVkp4K0xjeHF2SVhNMFR5bysvQ3UxenJmYW85OGFpQUNQOCtUU0VEaUZRTnRFdXMy + M0grZC9YMWhxTXdSSERJM2tRKzZzY2dFR25xWTU3cjNSRFNBM0U4RWhIcjQ9LGl2Okx4aX + RWSVltOHNyWlZxRnVlSmg5bG9DbEE0NFkyWjNYQVZZbXhlc01tT2c9LHRhZzpZOHFGRDhV + R2xEZndOU3Y3eGxjbjZBPT0sdHlwZTpzdHJdIiwKCQkicGdwIjogbnVsbCwKCQkidW5lbm + NyeXB0ZWRfc3VmZml4IjogIl91bmVuY3J5cHRlZCIsCgkJInZlcnNpb24iOiAiMy43LjEi + Cgl9Cn0= +`, + expected: `apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + name: my-secret +type: Opaque +`, + }, + { + name: "secret with basic auth", + yamlStr: `apiVersion: v1 +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +data: + username: admin + password: password +`, + expected: `apiVersion: v1 +data: + password: password + username: admin +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := yaml.Parse(tc.yamlStr) + if err != nil { + t.Fatalf("unable to parse yaml: %v", err) + } + + resource := &resource.Resource{RNode: *r} + err = trimSopsData(resource) + if err != nil { + t.Fatalf("unable to trim sops data: %v", err) + } + + sYaml, err := resource.AsYAML() + if err != nil { + t.Fatalf("unable to convert sanitized resources to yaml: %v", err) + } + if diff := cmp.Diff(string(sYaml), tc.expected); diff != "" { + t.Errorf("unexpected sanitized resources: (-got +want)%v", diff) + } + }) + } +} diff --git a/internal/kustomization/kustomization.go b/internal/kustomization/kustomization.go new file mode 100644 index 00000000..537d0011 --- /dev/null +++ b/internal/kustomization/kustomization.go @@ -0,0 +1,84 @@ +/* +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 ( + 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 +} diff --git a/internal/kustomization/kustomization_generator.go b/internal/kustomization/kustomization_generator.go new file mode 100644 index 00000000..829c3bdf --- /dev/null +++ b/internal/kustomization/kustomization_generator.go @@ -0,0 +1,258 @@ +/* +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 ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "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" +) + +type KustomizeGenerator struct { + kustomization Kustomize +} + +func NewGenerator(kustomization Kustomize) *KustomizeGenerator { + return &KustomizeGenerator{ + kustomization: kustomization, + } +} + +// 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 + } + + kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName()) + + data, err := os.ReadFile(kfile) + if err != nil { + return nil, err + } + + kus := kustypes.Kustomization{ + TypeMeta: kustypes.TypeMeta{ + APIVersion: kustypes.KustomizationVersion, + Kind: kustypes.KustomizationKind, + }, + } + + if err := yaml.Unmarshal(data, &kus); err != nil { + return nil, err + } + + if kg.kustomization.GetTargetNamespace() != "" { + kus.Namespace = kg.kustomization.GetTargetNamespace() + } + + for _, m := range kg.kustomization.GetPatches() { + kus.Patches = append(kus.Patches, kustypes.Patch{ + Patch: m.Patch, + Target: adaptSelector(&m.Target), + }) + } + + for _, m := range kg.kustomization.GetPatchesStrategicMerge() { + kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(m.Raw)) + } + + for _, m := range kg.kustomization.GetPatchesJSON6902() { + patch, err := json.Marshal(m.Patch) + if err != nil { + return nil, err + } + kus.PatchesJson6902 = append(kus.PatchesJson6902, kustypes.Patch{ + Patch: string(patch), + Target: adaptSelector(&m.Target), + }) + } + + for _, image := range kg.kustomization.GetImages() { + 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 { + return nil, err + } + + os.WriteFile(kfile, manifest, 0644) + + return data, nil +} + +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) generateKustomization(dirPath 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 + } + } + + 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 err + } + + files, err := scan(abs) + if err != nil { + return err + } + + kfile := filepath.Join(dirPath, konfig.DefaultKustomizationFileName()) + f, err := fs.Create(kfile) + if err != nil { + return 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 { + return err + } + + return 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) +} diff --git a/internal/kustomization/kustomization_varsub.go b/internal/kustomization/kustomization_varsub.go new file mode 100644 index 00000000..5a893eb9 --- /dev/null +++ b/internal/kustomization/kustomization_varsub.go @@ -0,0 +1,119 @@ +/* +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" + corev1 "k8s.io/api/core/v1" + "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 Kustomize, + 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 + } + + vars := make(map[string]string) + + // load vars from ConfigMaps and Secrets data keys + for _, reference := range kustomization.GetSubstituteFrom() { + 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) + } + } + } + + // 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 +} From 9376c9a946abdea91444e508b2c73ee0a63db5f3 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 8 Dec 2021 17:22:10 +0100 Subject: [PATCH 2/4] Add a diff kustomization feature If implemented it will permit queriying the Kubernetes API to fetch the specified Flux Kustomization, then uses the specified path to build the overlay. It will then ssa-dry-run apply and output the diff using homeport/dyff Signed-off-by: Soule BA --- cmd/flux/build_kustomization.go | 21 +- cmd/flux/diff.go | 31 +++ cmd/flux/diff_kustomization.go | 77 +++++++ go.mod | 27 ++- go.sum | 63 +++++- internal/kustomization/build.go | 28 ++- internal/kustomization/build_test.go | 12 +- internal/kustomization/diff.go | 287 +++++++++++++++++++++++++++ 8 files changed, 517 insertions(+), 29 deletions(-) create mode 100644 cmd/flux/diff.go create mode 100644 cmd/flux/diff_kustomization.go create mode 100644 internal/kustomization/diff.go diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index d4d8a8a0..9262355c 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -30,22 +30,23 @@ var buildKsCmd = &cobra.Command{ Use: "kustomization", Aliases: []string{"ks"}, Short: "Build Kustomization", - Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization, -then it uses the specified files or path to build the overlay to write the resulting multi-doc YAML to stdout.`, + Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. +It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml +pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, Example: `# Create a new overlay. -flux build kustomization my-app --resources ./path/to/local/manifests`, +flux build kustomization my-app --path ./path/to/local/manifests`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, } type buildKsFlags struct { - resources string + path string } var buildKsArgs buildKsFlags func init() { - buildKsCmd.Flags().StringVar(&buildKsArgs.resources, "resources", "", "Name of a file containing a file to add to the kustomization file.)") + buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.)") buildCmd.AddCommand(buildKsCmd) } @@ -55,15 +56,15 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { } name := args[0] - if buildKsArgs.resources == "" { - return fmt.Errorf("invalid resource path %q", buildKsArgs.resources) + if buildKsArgs.path == "" { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - if fs, err := os.Stat(buildKsArgs.resources); err != nil || !fs.IsDir() { - return fmt.Errorf("invalid resource path %q", buildKsArgs.resources) + if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, buildKsArgs.resources, kustomization.WithTimeout(rootArgs.timeout)) + builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, buildKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) if err != nil { return err } diff --git a/cmd/flux/diff.go b/cmd/flux/diff.go new file mode 100644 index 00000000..d4ee4804 --- /dev/null +++ b/cmd/flux/diff.go @@ -0,0 +1,31 @@ +/* +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 main + +import ( + "github.com/spf13/cobra" +) + +var diffCmd = &cobra.Command{ + Use: "diff", + Short: "Diff a flux resource", + Long: "The diff command is used to do a server-side dry-run on flux resources, then output the diff.", +} + +func init() { + rootCmd.AddCommand(diffCmd) +} diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go new file mode 100644 index 00000000..bfc5a195 --- /dev/null +++ b/cmd/flux/diff_kustomization.go @@ -0,0 +1,77 @@ +/* +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 main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/kustomization" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" +) + +var diffKsCmd = &cobra.Command{ + Use: "kustomization", + Aliases: []string{"ks"}, + Short: "Diff Kustomization", + Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, + Example: `# Create a new overlay. +flux diff kustomization my-app --path ./path/to/local/manifests`, + ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), + RunE: diffKsCmdRun, +} + +type diffKsFlags struct { + path string +} + +var diffKsArgs diffKsFlags + +func init() { + diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Name of a file containing a file to add to the kustomization file.)") + diffCmd.AddCommand(diffKsCmd) +} + +func diffKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if diffKsArgs.path == "" { + return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + } + + if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + } + + builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, diffKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) + if err != nil { + return err + } + + err = builder.Diff() + if err != nil { + return err + } + + return nil + +} diff --git a/go.mod b/go.mod index 86cb5883..6ea59ef9 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +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/meta v0.10.2 github.com/fluxcd/pkg/runtime v0.12.3 github.com/fluxcd/pkg/ssa v0.10.0 @@ -20,9 +21,13 @@ require ( github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/source-controller/api v0.20.1 github.com/go-git/go-git/v5 v5.4.2 + github.com/gonvenience/bunt v1.3.2 + 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-retryablehttp v0.7.0 // indirect + github.com/homeport/dyff v1.4.6 + github.com/lucasb-eyer/go-colorful v1.2.0 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-shellwords v1.0.12 github.com/olekukonko/tablewriter v0.0.4 @@ -42,6 +47,11 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require ( + github.com/drone/envsubst v1.0.3 + sigs.k8s.io/kustomize/kyaml v0.13.0 +) + require ( cloud.google.com/go v0.81.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -51,6 +61,7 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/BurntSushi/toml v0.4.1 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect github.com/Microsoft/go-winio v0.4.16 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -64,7 +75,6 @@ require ( github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect - github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect @@ -76,6 +86,10 @@ require ( github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/gonvenience/neat v1.3.7 // indirect + github.com/gonvenience/term v1.0.1 // indirect + github.com/gonvenience/text v1.0.6 // indirect + github.com/gonvenience/wrap v1.1.0 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-github/v41 v41.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -87,7 +101,6 @@ require ( github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -96,9 +109,13 @@ require ( github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -109,15 +126,18 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/testify v1.7.0 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect github.com/xanzy/go-gitlab v0.54.3 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect @@ -132,7 +152,6 @@ require ( k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect - sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect ) diff --git a/go.sum b/go.sum index dd909e60..adaf1efb 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= @@ -192,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-0.20200804185402-58bc65f69603 h1:PMzPM0wCHDrXlO7TlEq5lqlhGKHEgSnUR4YMSEVKrQ0= -github.com/drone/envsubst v1.0.3-0.20200804185402-58bc65f69603/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= +github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= 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= @@ -361,6 +363,22 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gonvenience/bunt v1.1.3/go.mod h1:hZ898ZprNWgaVlq4s1ivsJu3AY+3wrlJadF5Gp7Yq2c= +github.com/gonvenience/bunt v1.3.1/go.mod h1:G+d3dJBxxOqV2oca96psgAnPABVC9QptEifjVqPu+mo= +github.com/gonvenience/bunt v1.3.2 h1:gDiyTDfPf87fCtIbFzvENrmlnDZYbENdhhRW2kun/tw= +github.com/gonvenience/bunt v1.3.2/go.mod h1:oTOZqb1TVL1KqZm57zUCJCiwUliNfY8+d3QndOVqxpg= +github.com/gonvenience/neat v1.3.6/go.mod h1:wv0eXmvwFfpuS5bpf2fIofXQvf8W7HTdSzKfGCYbIe8= +github.com/gonvenience/neat v1.3.7 h1:k4shy3sgSBfUk9LTN51naxVIkB6BlGaxH0ReERO92zw= +github.com/gonvenience/neat v1.3.7/go.mod h1:Y4eeQH3GEBvjmLoMiu4oWGyOopGDaI2/y2jwcVfvlvs= +github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= +github.com/gonvenience/term v1.0.1 h1:8bg2O0ox0Ss64nnUxW5AXlSHhllc8dTOSKuKu6uoGpw= +github.com/gonvenience/term v1.0.1/go.mod h1:TrQEhxBNE/ng5kTV+S0OvQowTDJSfhlBeZbcOmTR6qI= +github.com/gonvenience/text v1.0.6 h1:9JH9fz0BL0NX4uKmjLuVcsBKiniPa+XLpf8KH9so44U= +github.com/gonvenience/text v1.0.6/go.mod h1:9U5WbkT/5wR5+aNMR4HucamY+HgVMEn+UbF78XHmUio= +github.com/gonvenience/wrap v1.1.0 h1:d8gEZrXS/zg4BC1q0U4nHpPIh5k6muKpQ1+rQFBwpYc= +github.com/gonvenience/wrap v1.1.0/go.mod h1:L47Cm1sK1G8QmFAYQfkHcF/sQ1IBJUa0u4sjqiLqPdM= +github.com/gonvenience/ytbx v1.4.2 h1:fpgOpsQ+gwTPqiatki0aY7q3BEjt7EcwiI5b+D0Qjvg= +github.com/gonvenience/ytbx v1.4.2/go.mod h1:GkUMPGH5qZSg1S8L6u9XNI9hJ4L1yKSQFIA4J8vaPdY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -424,6 +442,7 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -468,6 +487,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/homeport/dyff v1.4.6 h1:ReC8Hi1I6SkmPmNOqGl9EUlZctx+6AloCzSulqwHge8= +github.com/homeport/dyff v1.4.6/go.mod h1:DBCaTwJUIQLNQxNOKTce/OgRxCwwa8erBdN88bBfb9Y= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -526,6 +547,9 @@ github.com/ktrysmt/go-bitbucket v0.9.34/go.mod h1:FWxy2UK7GlK5b0NSJGc5hPqnssVlkN github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -539,9 +563,15 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -556,10 +586,15 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= +github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -600,6 +635,11 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= @@ -609,6 +649,10 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= +github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= @@ -670,8 +714,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -720,12 +765,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db/go.mod h1:grWy0bkr1XO6hqbaaCKaPXqkBVlMGHYG6PGykktwbJc= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xanzy/go-gitlab v0.54.3 h1:fPfZ3Jcu5dPc3xyIYtAALZsEgoyKNFNuULD+TdJ7Zvk= github.com/xanzy/go-gitlab v0.54.3/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= @@ -865,6 +914,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -880,6 +930,7 @@ golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917163549-3c21e5b27794/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -912,6 +963,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -925,6 +977,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -948,6 +1001,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -990,11 +1044,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/kustomization/build.go b/internal/kustomization/build.go index 4b5279a4..b2704c80 100644 --- a/internal/kustomization/build.go +++ b/internal/kustomization/build.go @@ -27,8 +27,10 @@ import ( "github.com/fluxcd/flux2/internal/utils" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/types" "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" @@ -44,6 +46,7 @@ var defaultTimeout = 80 * time.Second // and overlays the manifests with the resources specified in the resourcesPath type Builder struct { client client.WithWatch + restMapper meta.RESTMapper name string namespace string resourcesPath string @@ -68,8 +71,18 @@ func NewBuilder(kubeconfig string, kubecontext string, namespace, name, resource return nil, err } + cfg, err := utils.KubeConfig(kubeconfig, kubecontext) + if err != nil { + return nil, err + } + restMapper, err := apiutil.NewDynamicRESTMapper(cfg) + if err != nil { + return nil, err + } + b := &Builder{ client: kubeClient, + restMapper: restMapper, name: name, namespace: namespace, resourcesPath: resources, @@ -154,8 +167,8 @@ func (b *Builder) build() (resmap.ResMap, error) { // store the kustomization object b.kustomization = k - // restore the kustomization.yaml - err = restore(saved, b.resourcesPath) + // 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) } @@ -205,24 +218,27 @@ func trimSopsData(res *resource.Resource) error { for k, v := range dataMap { data, err := base64.StdEncoding.DecodeString(v) if err != nil { - fmt.Println(fmt.Errorf("failed to decode secret data: %w", err)) + if _, ok := err.(base64.CorruptInputError); ok { + return fmt.Errorf("failed to decode secret data: %w", err) + } } - if bytes.Contains(data, []byte("sops")) { + if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { dataMap[k] = sopsMess } } + res.SetDataMap(dataMap) } return nil } -func restore(saved []byte, dirPath string) error { +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 restore kustomization.yaml: %w", err) + return fmt.Errorf("failed to overwrite kustomization.yaml: %w", err) } return nil } diff --git a/internal/kustomization/build_test.go b/internal/kustomization/build_test.go index 7834e108..f39b22aa 100644 --- a/internal/kustomization/build_test.go +++ b/internal/kustomization/build_test.go @@ -24,7 +24,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) -func TestSanitizeResources(t *testing.T) { +func TestTrimSopsData(t *testing.T) { testCases := []struct { name string yamlStr string @@ -75,18 +75,18 @@ type: Opaque { name: "secret with basic auth", yamlStr: `apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4K kind: Secret metadata: name: secret-basic-auth type: kubernetes.io/basic-auth -data: - username: admin - password: password `, expected: `apiVersion: v1 data: - password: password - username: admin + password: cGFzc3dvcmQK + username: YWRtaW4K kind: Secret metadata: name: secret-basic-auth diff --git a/internal/kustomization/diff.go b/internal/kustomization/diff.go new file mode 100644 index 00000000..4bf2f982 --- /dev/null +++ b/internal/kustomization/diff.go @@ -0,0 +1,287 @@ +package kustomization + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/ssa" + "github.com/gonvenience/bunt" + "github.com/gonvenience/ytbx" + "github.com/google/go-cmp/cmp" + "github.com/homeport/dyff/pkg/dyff" + "github.com/lucasb-eyer/go-colorful" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/yaml" +) + +const ( + controllerName = "kustomize-controller" + controllerGroup = "kustomize.toolkit.fluxcd.io" +) + +func (b *Builder) manager() (*ssa.ResourceManager, error) { + statusPoller := polling.NewStatusPoller(b.client, b.restMapper) + owner := ssa.Owner{ + Field: controllerName, + Group: controllerGroup, + } + + return ssa.NewResourceManager(b.client, statusPoller, owner), nil +} + +func (b *Builder) Diff() error { + res, err := b.Build() + if err != nil { + return err + } + // convert the build result into Kubernetes unstructured objects + objects, err := ssa.ReadObjects(bytes.NewReader(res)) + if err != nil { + return err + } + + resourceManager, err := b.manager() + if err != nil { + return err + } + + resourceManager.SetOwnerLabels(objects, b.kustomization.GetName(), b.kustomization.GetNamespace()) + + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + if err := ssa.SetNativeKindsDefaults(objects); err != nil { + return err + } + + // create an inventory of objects to be reconciled + newInventory := newInventory() + 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) + } else { + writeString(fmt.Sprint(`✗`, err), bunt.Red) + } + continue + } + + // if the object is a sops secret, we need to + // make sure we diff only if the keys are different + if obj.GetKind() == "Secret" && change.Action == string(ssa.ConfiguredAction) { + diffSopsSecret(obj, liveObject, mergedObject, change) + } + + if change.Action == string(ssa.CreatedAction) { + writeString(fmt.Sprintf("► %s created", change.Subject), bunt.Green) + } + + if change.Action == string(ssa.ConfiguredAction) { + writeString(fmt.Sprintf("► %s drifted", change.Subject), bunt.WhiteSmoke) + liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) + if err != nil { + return err + } + defer cleanupDir(tmpDir) + + err = diff(liveFile, mergedFile) + if err != nil { + return err + } + } + + addObjectsToInventory(newInventory, change) + } + + if b.kustomization.Spec.Prune { + oldStatus := b.kustomization.Status.DeepCopy() + if oldStatus.Inventory != nil { + diffObjects, err := diffInventory(oldStatus.Inventory, newInventory) + if err != nil { + return err + } + for _, object := range diffObjects { + writeString(fmt.Sprintf("► %s deleted", ssa.FmtUnstructured(object)), bunt.OrangeRed) + } + } + } + + return nil +} + +func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + return "", "", "", err + } + + liveYAML, _ := yaml.Marshal(liveObject) + liveFile := filepath.Join(tmpDir, "live.yaml") + if err := os.WriteFile(liveFile, liveYAML, 0644); err != nil { + return "", "", "", err + } + + mergedYAML, _ := yaml.Marshal(mergedObject) + mergedFile := filepath.Join(tmpDir, "merged.yaml") + if err := os.WriteFile(mergedFile, mergedYAML, 0644); err != nil { + return "", "", "", err + } + + return liveFile, mergedFile, tmpDir, nil +} + +func writeString(t string, color colorful.Color) { + fmt.Println(bunt.Style( + t, + bunt.EachLine(), + bunt.Foreground(color), + )) +} + +func cleanupDir(dir string) error { + return os.RemoveAll(dir) +} + +func diff(liveFile, mergedFile string) error { + from, to, err := ytbx.LoadFiles(liveFile, mergedFile) + if err != nil { + return fmt.Errorf("failed to load input files: %w", err) + } + + report, err := dyff.CompareInputFiles(from, to, + dyff.IgnoreOrderChanges(false), + dyff.KubernetesEntityDetection(true), + ) + if err != nil { + return fmt.Errorf("failed to compare input files: %w", err) + } + + reportWriter := &dyff.HumanReport{ + Report: report, + OmitHeader: true, + } + + if err := reportWriter.WriteReport(os.Stdout); err != nil { + return fmt.Errorf("failed to print report: %w", err) + } + + return nil +} + +func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { + data := obj.Object["data"] + for _, v := range data.(map[string]interface{}) { + v, err := base64.StdEncoding.DecodeString(v.(string)) + if err != nil { + fmt.Println(err) + } + if bytes.Contains(v, []byte(mask)) { + if liveObject != nil && mergedObject != nil { + change.Action = string(ssa.UnchangedAction) + dataLive := liveObject.Object["data"].(map[string]interface{}) + dataMerged := mergedObject.Object["data"].(map[string]interface{}) + if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" { + change.Action = string(ssa.ConfiguredAction) + } + } + } + } +} + +func keys(m map[string]interface{}) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = k + i++ + } + return keys +} + +// diffInventory returns the slice of objects that do not exist in the target inventory. +func diffInventory(inv *kustomizev1.ResourceInventory, target *kustomizev1.ResourceInventory) ([]*unstructured.Unstructured, error) { + versionOf := func(i *kustomizev1.ResourceInventory, objMetadata object.ObjMetadata) string { + for _, entry := range i.Entries { + if entry.ID == objMetadata.String() { + return entry.Version + } + } + return "" + } + + objects := make([]*unstructured.Unstructured, 0) + aList, err := listMetaInInventory(inv) + if err != nil { + return nil, err + } + + bList, err := listMetaInInventory(target) + if err != nil { + return nil, err + } + + list := aList.Diff(bList) + if len(list) == 0 { + return objects, nil + } + + for _, metadata := range list { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.GroupKind.Group, + Kind: metadata.GroupKind.Kind, + Version: versionOf(inv, metadata), + }) + u.SetName(metadata.Name) + u.SetNamespace(metadata.Namespace) + objects = append(objects, u) + } + + sort.Sort(ssa.SortableUnstructureds(objects)) + return objects, nil +} + +// listMetaInInventory returns the inventory entries as object.ObjMetadata objects. +func listMetaInInventory(inv *kustomizev1.ResourceInventory) (object.ObjMetadataSet, error) { + var metas []object.ObjMetadata + for _, e := range inv.Entries { + m, err := object.ParseObjMetadata(e.ID) + if err != nil { + return metas, err + } + metas = append(metas, m) + } + + return metas, nil +} + +func newInventory() *kustomizev1.ResourceInventory { + return &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{}, + } +} + +// addObjectsToInventory extracts the metadata from the given objects and adds it to the inventory. +func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.ChangeSetEntry) error { + if entry == nil { + return nil + } + + inv.Entries = append(inv.Entries, kustomizev1.ResourceRef{ + ID: entry.ObjMetadata.String(), + Version: entry.GroupVersion, + }) + + return nil +} From f7d9ee90cdfaa7769dd35cae74a2d743f4d61359 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 15 Dec 2021 16:23:54 +0100 Subject: [PATCH 3/4] Add e2e tests for build/diff kustomization Signed-off-by: Soule BA --- cmd/flux/build_kustomization.go | 2 +- cmd/flux/build_kustomization_test.go | 83 +++++ cmd/flux/diff_kustomization.go | 6 +- cmd/flux/diff_kustomization_test.go | 129 +++++++ cmd/flux/main_test.go | 40 ++- .../delete-service/deployment.yaml | 74 ++++ .../delete-service/hpa.yaml | 20 ++ .../delete-service/kustomization.yaml | 5 + .../podinfo-kustomization.yaml | 15 + .../build-kustomization/podinfo-result.yaml | 148 ++++++++ .../build-kustomization/podinfo-source.yaml | 16 + .../podinfo-without-service-result.yaml | 101 ++++++ .../podinfo/deployment.yaml | 74 ++++ .../build-kustomization/podinfo/hpa.yaml | 20 ++ .../podinfo/kustomization.yaml | 14 + .../build-kustomization/podinfo/service.yaml | 17 + .../podinfo/token.encrypted | 20 ++ .../diff-kustomization/deployment.yaml | 78 ++++ .../diff-with-deployment.golden | 4 + .../diff-with-drifted-key-sops-secret.golden | 10 + .../diff-with-drifted-secret.golden | 11 + .../diff-with-drifted-service.golden | 16 + ...diff-with-drifted-value-sops-secret.golden | 4 + .../diff-kustomization/key-sops-secret.yaml | 11 + .../diff-kustomization/kustomization.yaml | 11 + .../nothing-is-deployed.golden | 5 + .../testdata/diff-kustomization/secret.yaml | 12 + .../testdata/diff-kustomization/service.yaml | 21 ++ .../diff-kustomization/value-sops-secret.yaml | 11 + go.mod | 2 +- internal/kustomization/build.go | 113 +++--- internal/kustomization/diff.go | 63 ++-- internal/kustomization/kustomization.go | 60 ---- .../kustomization/kustomization_generator.go | 338 ++++++++++++++++-- .../kustomization/kustomization_varsub.go | 119 ++++-- 35 files changed, 1467 insertions(+), 206 deletions(-) create mode 100644 cmd/flux/build_kustomization_test.go create mode 100644 cmd/flux/diff_kustomization_test.go create mode 100644 cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml create mode 100644 cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml create mode 100644 cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo-result.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo-source.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo/service.yaml create mode 100644 cmd/flux/testdata/build-kustomization/podinfo/token.encrypted create mode 100644 cmd/flux/testdata/diff-kustomization/deployment.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden create mode 100644 cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden create mode 100644 cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden create mode 100644 cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden create mode 100644 cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden create mode 100644 cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/kustomization.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden create mode 100644 cmd/flux/testdata/diff-kustomization/secret.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/service.yaml create mode 100644 cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 9262355c..0cebfd0d 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -64,7 +64,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, buildKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) + builder, err := kustomization.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) if err != nil { return err } diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go new file mode 100644 index 00000000..99ecb45c --- /dev/null +++ b/cmd/flux/build_kustomization_test.go @@ -0,0 +1,83 @@ +//go:build unit +// +build unit + +/* +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 main + +import ( + "testing" +) + +func setup(t *testing.T, tmpl map[string]string) { + t.Helper() + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t) +} + +func TestBuildKustomization(t *testing.T) { + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "build kustomization podinfo", + resultFile: "invalid resource path \"\"", + assertFunc: "assertError", + }, + { + name: "build podinfo", + args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build podinfo without service", + args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", + resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index bfc5a195..4a2afed5 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -62,16 +62,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", diffKsArgs.path) } - builder, err := kustomization.NewBuilder(rootArgs.kubeconfig, rootArgs.kubecontext, rootArgs.namespace, name, diffKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) + builder, err := kustomization.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) if err != nil { return err } - err = builder.Diff() + output, err := builder.Diff() if err != nil { return err } + cmd.Print(output) + return nil } diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go new file mode 100644 index 00000000..c7a11d36 --- /dev/null +++ b/cmd/flux/diff_kustomization_test.go @@ -0,0 +1,129 @@ +//go:build unit +// +build unit + +/* +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 main + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/fluxcd/flux2/internal/kustomization" + "github.com/fluxcd/pkg/ssa" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestDiffKustomization(t *testing.T) { + tests := []struct { + name string + args string + objectFile string + assert assertFunc + }{ + { + name: "no args", + args: "diff kustomization podinfo", + objectFile: "", + assert: assertError("invalid resource path \"\""), + }, + { + name: "diff nothing deployed", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "", + assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), + }, + { + name: "diff with a deployment object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/deployment.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-deployment.golden"), + }, + { + name: "diff with a drifted service object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/service.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-service.golden"), + }, + { + name: "diff with a drifted secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-secret.golden"), + }, + { + name: "diff with a drifted key in sops secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/key-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden"), + }, + { + name: "diff with a drifted value in sops secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"), + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + + b, _ := kustomization.NewBuilder(kubeconfigArgs, "podinfo", "") + + resourceManager, err := b.Manager() + if err != nil { + t.Fatal(err) + } + + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.objectFile != "" { + resourceManager.ApplyAll(context.Background(), createObjectFromFile(tt.objectFile, tmpl, t), ssa.DefaultApplyOptions()) + } + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: tt.assert, + } + cmd.runTestCmd(t) + if tt.objectFile != "" { + testEnv.DeleteObjectFile(tt.objectFile, tmpl, t) + } + }) + } +} + +func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) + } + + return clientObjects +} diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 10879e23..26b4e99e 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -49,8 +49,8 @@ func allocateNamespace(prefix string) string { return fmt.Sprintf("%s-%d", prefix, id) } -func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { - objects := []unstructured.Unstructured{} +func readYamlObjects(rdr io.Reader) ([]*unstructured.Unstructured, error) { + objects := []*unstructured.Unstructured{} reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr)) for { doc, err := reader.Read() @@ -65,7 +65,7 @@ func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { if err != nil { return nil, err } - objects = append(objects, *unstructuredObj) + objects = append(objects, unstructuredObj) } return objects, nil } @@ -96,7 +96,7 @@ func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues } } -func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured, t *testing.T) error { +func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { for _, obj := range clientObjects { // First create the object then set its status if present in the // yaml file. Make a copy first since creating an object may overwrite @@ -107,7 +107,7 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstruct return err } obj.SetResourceVersion(createObj.GetResourceVersion()) - err = m.client.Status().Update(context.Background(), &obj) + err = m.client.Status().Update(context.Background(), obj) if err != nil { return err } @@ -115,6 +115,36 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstruct return nil } +func (m *testEnvKubeManager) DeleteObjectFile(objectFile string, templateValues map[string]string, t *testing.T) { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) + } + err = m.DeleteObjects(clientObjects, t) + if err != nil { + t.Logf("Error deleting test objects: '%v'", err) + } +} + +func (m *testEnvKubeManager) DeleteObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { + for _, obj := range clientObjects { + err := m.client.Delete(context.Background(), obj) + if err != nil { + return err + } + } + + return nil +} + func (m *testEnvKubeManager) Stop() error { if m.testEnv == nil { return fmt.Errorf("do nothing because testEnv is nil") diff --git a/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml new file mode 100644 index 00000000..33a65a3a --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.3 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml new file mode 100644 index 00000000..f8111598 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml new file mode 100644 index 00000000..1d0e99c5 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml diff --git a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml new file mode 100644 index 00000000..036185dc --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/podinfo-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml new file mode 100644 index 00000000..ce66ff0f --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml @@ -0,0 +1,148 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + progressDeadlineSeconds: 60 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: podinfo + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + prometheus.io/port: "9797" + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: '#34577c' + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + name: podinfod + ports: + - containerPort: 9898 + name: http + protocol: TCP + - containerPort: 9797 + name: http-metrics + protocol: TCP + - containerPort: 9999 + name: grpc + protocol: TCP + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi +--- +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo +--- +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - name: grpc + port: 9999 + protocol: TCP + targetPort: grpc + selector: + app: podinfo + type: ClusterIP +--- +apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque +--- +apiVersion: v1 +data: + password: MWYyZDFlMmU2N2Rm + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: db-user-pass-bkbd782d2c + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml new file mode 100644 index 00000000..f1a33ecd --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1beta1 +kind: GitRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 30s + ref: + branch: master + url: https://github.com/stefanprodan/podinfo \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml new file mode 100644 index 00000000..943381c8 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + progressDeadlineSeconds: 60 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: podinfo + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + prometheus.io/port: "9797" + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: '#34577c' + image: ghcr.io/stefanprodan/podinfo:6.0.3 + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + name: podinfod + ports: + - containerPort: 9898 + name: http + protocol: TCP + - containerPort: 9797 + name: http-metrics + protocol: TCP + - containerPort: 9999 + name: grpc + protocol: TCP + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi +--- +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml new file mode 100644 index 00000000..1a3287bd --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml new file mode 100644 index 00000000..f8111598 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml new file mode 100644 index 00000000..0ba07668 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +secretGenerator: + - files: + - token=token.encrypted + name: podinfo-token + - literals: + - username=admin + - password=1f2d1e2e67df + name: db-user-pass diff --git a/cmd/flux/testdata/build-kustomization/podinfo/service.yaml b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml new file mode 100644 index 00000000..0d26eca3 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: podinfo +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc \ No newline at end of file diff --git a/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted new file mode 100644 index 00000000..c88ac972 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted @@ -0,0 +1,20 @@ + { + "data": "ENC[AES256_GCM,data:oBe5PlPmfQCUUc4sqKImjw==,iv:MLLEW15QC9kRdVVagJnzLCSk0xZGWIpAeTfHzyxT10g=,tag:K3GkBCGS+ut4Tpk6ndb0CA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+ IFgyNTUxOSA1L2RpZHRrK1FSVmYrd1Va\nY0hxWFQzSDBsT1k3WjNtYmU1QmliaDJycXlNCnF1YjdNOThVbVNvMG9rNS9ZUXZw\nMnV0bnRUMGNtejFPbzM4U2UzWkszeVkKLS0tIGJ6UGhxMUV3YmVJTHlJSUJpRVRZ\nVjd0RVRadU8wekxXTHIrYUplYkN2aEEK0I/ MCEtXRk+b/N2G1JF3vHQT24dShWYD\nw+JIUSA3aLf2sv0zr2MdUEdVWBJoM8nT4D4xVbBORD+669W+9nDeSw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2021-11-26T16:34:51Z", + "mac": "ENC[AES256_GCM,data:COGzf5YCHNNP6z4JaEKrjN3M8f5+Q1uKUKTMHwj388/ICmLyi2sSrTmj7PP+X7M9jTVwa8wVgYTpNLiVJx+LcxqvIXM0Tyo+/Cu1zrfao98aiACP8+TSEDiFQNtEus23H+d/X1hqMwRHDI3kQ+ 6scgEGnqY57r3RDSA3E8EhHr4=,iv:LxitVIYm8srZVqFueJh9loClA44Y2Z3XAVYmxesMmOg=,tag:Y8qFD8UGlDfwNSv7xlcn6A==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.1" + } + } \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/deployment.yaml b/cmd/flux/testdata/diff-kustomization/deployment.yaml new file mode 100644 index 00000000..9b6b6e1b --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden new file mode 100644 index 00000000..098497fc --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden @@ -0,0 +1,4 @@ +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden new file mode 100644 index 00000000..38269544 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden @@ -0,0 +1,10 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 drifted + +data + - one map entry removed: + one map entry added: + drift-key: "*****" token: "*****" + +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden new file mode 100644 index 00000000..ac76978f --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden @@ -0,0 +1,11 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c drifted + +data.password + ± value change + - ****** + + ***** + diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden new file mode 100644 index 00000000..d65e5968 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden @@ -0,0 +1,16 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo drifted + +spec.ports + ⇆ order changed + - http, grpc + + grpc, http + +spec.ports.http.port + ± value change + - 9899 + + 9898 + +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden new file mode 100644 index 00000000..033db67e --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden @@ -0,0 +1,4 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml new file mode 100644 index 00000000..52f7cf46 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + drift-key: bXktc2VjcmV0LXRva2VuCg== +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/kustomization.yaml b/cmd/flux/testdata/diff-kustomization/kustomization.yaml new file mode 100644 index 00000000..dfe99e32 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +secretGenerator: + - literals: + - username=admin + - password=1f2d1e2e67df + name: secret-basic-auth \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden new file mode 100644 index 00000000..da1c23da --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden @@ -0,0 +1,5 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/secret.yaml b/cmd/flux/testdata/diff-kustomization/secret.yaml new file mode 100644 index 00000000..3911cf0c --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: db-user-pass-bkbd782d2c + namespace: default +type: Opaque \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/service.yaml b/cmd/flux/testdata/diff-kustomization/service.yaml new file mode 100644 index 00000000..640fbd2f --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9899 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml new file mode 100644 index 00000000..1a469b25 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + token: ZHJpZnQtdmFsdWUK +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque \ No newline at end of file diff --git a/go.mod b/go.mod index 6ea59ef9..e3cb91ad 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +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-retryablehttp v0.7.0 // indirect github.com/homeport/dyff v1.4.6 github.com/lucasb-eyer/go-colorful v1.2.0 @@ -100,7 +101,6 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect diff --git a/internal/kustomization/build.go b/internal/kustomization/build.go index b2704c80..1b17ed41 100644 --- a/internal/kustomization/build.go +++ b/internal/kustomization/build.go @@ -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 + return } + // store the kustomization object + b.kustomization = k + // 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) + 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 } + defer func() { + errf := CleanDirectory(b.resourcesPath, action) + if err == nil { + err = errf + } + }() + // build the kustomization - m, err := b.do(ctx, *k, b.resourcesPath) + m, err = b.do(ctx, *k, b.resourcesPath) if err != nil { - return nil, err + return } - // make sure secrets are masked for _, res := range m.Resources() { - err := trimSopsData(res) + // set owner labels + err = b.setOwnerLabels(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) + // make sure secrets are masked + err = trimSopsData(res) + if err != nil { + return + } } - return m, nil + 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 -} diff --git a/internal/kustomization/diff.go b/internal/kustomization/diff.go index 4bf2f982..f084b56b 100644 --- a/internal/kustomization/diff.go +++ b/internal/kustomization/diff.go @@ -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 +} diff --git a/internal/kustomization/kustomization.go b/internal/kustomization/kustomization.go index 537d0011..c16afc99 100644 --- a/internal/kustomization/kustomization.go +++ b/internal/kustomization/kustomization.go @@ -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 -} diff --git a/internal/kustomization/kustomization_generator.go b/internal/kustomization/kustomization_generator.go index 829c3bdf..fc70fb8d 100644 --- a/internal/kustomization/kustomization_generator.go +++ b/internal/kustomization/kustomization_generator.go @@ -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 + } + + 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 _, m := range kg.kustomization.GetPatches() { + 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 _, p := range patchesSM { + kus.PatchesStrategicMerge = append(kus.PatchesStrategicMerge, kustypes.PatchStrategicMerge(p.Raw)) } - for _, m := range kg.kustomization.GetPatchesJSON6902() { - patch, err := json.Marshal(m.Patch) + 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 + } + + 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 } - os.WriteFile(kfile, manifest, 0644) + 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 +} diff --git a/internal/kustomization/kustomization_varsub.go b/internal/kustomization/kustomization_varsub.go index 5a893eb9..f89e3273 100644 --- a/internal/kustomization/kustomization_varsub.go +++ b/internal/kustomization/kustomization_varsub.go @@ -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 } + // 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) + } - // load vars from ConfigMaps and Secrets data keys - for _, reference := range kustomization.GetSubstituteFrom() { + 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) + 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) } } - // 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(data), func(s string) string { + return vars[s] + }) + if err != nil { + return nil, fmt.Errorf("variable substitution failed: %w", err) + } - 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) + } - jsonData, err := yaml.YAMLToJSON([]byte(output)) - if err != nil { - return nil, fmt.Errorf("YAMLToJSON: %w", err) - } + return jsonData, nil +} - err = res.UnmarshalJSON(jsonData) - if err != nil { - return nil, fmt.Errorf("UnmarshalJSON: %w", err) +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 res, nil + return nil, resultErr + } From 306f8f57157715612b80230517bbc57926eb048c Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 12 Jan 2022 11:50:19 +0100 Subject: [PATCH 4/4] Add graceful shutdown when interrupted If implemented this permit restoring a clean state in case of signal interruption. Signed-off-by: Soule BA --- cmd/flux/build_kustomization.go | 35 +- cmd/flux/diff_kustomization.go | 37 +- cmd/flux/diff_kustomization_test.go | 4 +- .../delete-service/deployment.yaml | 2 +- .../delete-service/hpa.yaml | 2 +- .../podinfo-kustomization.yaml | 2 +- .../build-kustomization/podinfo-source.yaml | 2 +- .../podinfo/deployment.yaml | 2 +- .../build-kustomization/podinfo/hpa.yaml | 2 +- .../build-kustomization/podinfo/service.yaml | 2 +- .../diff-kustomization/deployment.yaml | 2 +- .../diff-kustomization/kustomization.yaml | 2 +- .../testdata/diff-kustomization/secret.yaml | 2 +- .../testdata/diff-kustomization/service.yaml | 2 +- .../diff-kustomization/value-sops-secret.yaml | 2 +- go.mod | 9 +- go.sum | 10 +- internal/{kustomization => build}/build.go | 54 +- .../{kustomization => build}/build_test.go | 2 +- internal/{kustomization => build}/diff.go | 27 +- internal/kustomization/kustomization.go | 24 - .../kustomization/kustomization_generator.go | 532 ------------------ .../kustomization/kustomization_varsub.go | 176 ------ 23 files changed, 134 insertions(+), 800 deletions(-) rename internal/{kustomization => build}/build.go (84%) rename internal/{kustomization => build}/build_test.go (99%) rename internal/{kustomization => build}/diff.go (92%) delete mode 100644 internal/kustomization/kustomization.go delete mode 100644 internal/kustomization/kustomization_generator.go delete mode 100644 internal/kustomization/kustomization_varsub.go diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 0cebfd0d..966046b7 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -19,10 +19,11 @@ package main import ( "fmt" "os" + "os/signal" "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/kustomization" + "github.com/fluxcd/flux2/internal/build" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" ) @@ -33,7 +34,7 @@ var buildKsCmd = &cobra.Command{ Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, - Example: `# Create a new overlay. + Example: `# Build the local manifests as they were built on the cluster flux build kustomization my-app --path ./path/to/local/manifests`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, @@ -64,18 +65,36 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - builder, err := kustomization.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) + builder, err := build.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout)) if err != nil { 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 4a2afed5..e40f72c9 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -19,10 +19,11 @@ package main import ( "fmt" "os" + "os/signal" "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/kustomization" + "github.com/fluxcd/flux2/internal/build" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" ) @@ -31,7 +32,7 @@ var diffKsCmd = &cobra.Command{ Aliases: []string{"ks"}, Short: "Diff Kustomization", Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, - Example: `# Create a new overlay. + Example: `# Preview changes local changes as they were applied on the cluster flux diff kustomization my-app --path ./path/to/local/manifests`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: diffKsCmdRun, @@ -44,7 +45,7 @@ type diffKsFlags struct { var diffKsArgs diffKsFlags func init() { - diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Name of a file containing a file to add to the kustomization file.)") + diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.)") diffCmd.AddCommand(diffKsCmd) } @@ -62,18 +63,36 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", diffKsArgs.path) } - builder, err := kustomization.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, kustomization.WithTimeout(rootArgs.timeout)) + builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) if err != nil { 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/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go index c7a11d36..e2f62e75 100644 --- a/cmd/flux/diff_kustomization_test.go +++ b/cmd/flux/diff_kustomization_test.go @@ -25,7 +25,7 @@ import ( "strings" "testing" - "github.com/fluxcd/flux2/internal/kustomization" + "github.com/fluxcd/flux2/internal/build" "github.com/fluxcd/pkg/ssa" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -85,7 +85,7 @@ func TestDiffKustomization(t *testing.T) { "fluxns": allocateNamespace("flux-system"), } - b, _ := kustomization.NewBuilder(kubeconfigArgs, "podinfo", "") + b, _ := build.NewBuilder(kubeconfigArgs, "podinfo", "") resourceManager, err := b.Manager() if err != nil { diff --git a/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml index 33a65a3a..25896dd8 100644 --- a/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml +++ b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml @@ -71,4 +71,4 @@ spec: memory: 512Mi requests: cpu: 100m - memory: 64Mi \ No newline at end of file + memory: 64Mi diff --git a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml index f8111598..6bd46a2c 100644 --- a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml +++ b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml @@ -17,4 +17,4 @@ spec: type: Utilization # scale up if usage is above # 99% of the requested CPU (100m) - averageUtilization: 99 \ No newline at end of file + averageUtilization: 99 diff --git a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml index 036185dc..6d3eabed 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml @@ -12,4 +12,4 @@ spec: sourceRef: kind: GitRepository name: podinfo - targetNamespace: default \ No newline at end of file + targetNamespace: default diff --git a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml index f1a33ecd..745dd650 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml @@ -13,4 +13,4 @@ spec: interval: 30s ref: branch: master - url: https://github.com/stefanprodan/podinfo \ No newline at end of file + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml index 1a3287bd..d96b3c3c 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml @@ -71,4 +71,4 @@ spec: memory: 512Mi requests: cpu: 100m - memory: 64Mi \ No newline at end of file + memory: 64Mi diff --git a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml index f8111598..6bd46a2c 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml @@ -17,4 +17,4 @@ spec: type: Utilization # scale up if usage is above # 99% of the requested CPU (100m) - averageUtilization: 99 \ No newline at end of file + averageUtilization: 99 diff --git a/cmd/flux/testdata/build-kustomization/podinfo/service.yaml b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml index 0d26eca3..9450823d 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo/service.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml @@ -14,4 +14,4 @@ spec: - port: 9999 targetPort: grpc protocol: TCP - name: grpc \ No newline at end of file + name: grpc diff --git a/cmd/flux/testdata/diff-kustomization/deployment.yaml b/cmd/flux/testdata/diff-kustomization/deployment.yaml index 9b6b6e1b..3910da6a 100644 --- a/cmd/flux/testdata/diff-kustomization/deployment.yaml +++ b/cmd/flux/testdata/diff-kustomization/deployment.yaml @@ -75,4 +75,4 @@ spec: memory: 512Mi requests: cpu: 100m - memory: 64Mi \ No newline at end of file + memory: 64Mi diff --git a/cmd/flux/testdata/diff-kustomization/kustomization.yaml b/cmd/flux/testdata/diff-kustomization/kustomization.yaml index dfe99e32..997ce70a 100644 --- a/cmd/flux/testdata/diff-kustomization/kustomization.yaml +++ b/cmd/flux/testdata/diff-kustomization/kustomization.yaml @@ -8,4 +8,4 @@ secretGenerator: - literals: - username=admin - password=1f2d1e2e67df - name: secret-basic-auth \ No newline at end of file + name: secret-basic-auth diff --git a/cmd/flux/testdata/diff-kustomization/secret.yaml b/cmd/flux/testdata/diff-kustomization/secret.yaml index 3911cf0c..cf0b351e 100644 --- a/cmd/flux/testdata/diff-kustomization/secret.yaml +++ b/cmd/flux/testdata/diff-kustomization/secret.yaml @@ -9,4 +9,4 @@ metadata: kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: db-user-pass-bkbd782d2c namespace: default -type: Opaque \ No newline at end of file +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/service.yaml b/cmd/flux/testdata/diff-kustomization/service.yaml index 640fbd2f..95068162 100644 --- a/cmd/flux/testdata/diff-kustomization/service.yaml +++ b/cmd/flux/testdata/diff-kustomization/service.yaml @@ -18,4 +18,4 @@ spec: - port: 9999 targetPort: grpc protocol: TCP - name: grpc \ No newline at end of file + name: grpc diff --git a/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml index 1a469b25..3ae9ada2 100644 --- a/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml +++ b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml @@ -8,4 +8,4 @@ metadata: kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} name: podinfo-token-77t89m9b67 namespace: default -type: Opaque \ No newline at end of file +type: Opaque diff --git a/go.mod b/go.mod index e3cb91ad..2ef0441c 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,10 @@ 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 + github.com/fluxcd/pkg/ssa v0.11.0 github.com/fluxcd/pkg/ssh v0.3.1 github.com/fluxcd/pkg/untar v0.0.5 github.com/fluxcd/pkg/version v0.0.1 @@ -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 adaf1efb..9f897faf 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,10 +240,12 @@ 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= -github.com/fluxcd/pkg/ssa v0.10.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= +github.com/fluxcd/pkg/ssa v0.11.0 h1:ejEMlHPsbXMP8BJQx3+0PwoBgJur0mHiPcMNKcFwoOE= +github.com/fluxcd/pkg/ssa v0.11.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244= github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ= github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk= diff --git a/internal/kustomization/build.go b/internal/build/build.go similarity index 84% rename from internal/kustomization/build.go rename to internal/build/build.go index 1b17ed41..075ec44c 100644 --- a/internal/kustomization/build.go +++ b/internal/build/build.go @@ -14,17 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kustomization +package build import ( "bytes" "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/build_test.go b/internal/build/build_test.go similarity index 99% rename from internal/kustomization/build_test.go rename to internal/build/build_test.go index f39b22aa..c19a81be 100644 --- a/internal/kustomization/build_test.go +++ b/internal/build/build_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kustomization +package build import ( "testing" diff --git a/internal/kustomization/diff.go b/internal/build/diff.go similarity index 92% rename from internal/kustomization/diff.go rename to internal/build/diff.go index f084b56b..70dbb116 100644 --- a/internal/kustomization/diff.go +++ b/internal/build/diff.go @@ -1,4 +1,4 @@ -package kustomization +package build import ( "bytes" @@ -18,7 +18,6 @@ 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" @@ -27,7 +26,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 +52,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() @@ -65,12 +62,17 @@ func (b *Builder) Diff() (string, error) { // create an inventory of objects to be reconciled newInventory := newInventory() for _, obj := range objects { - change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj) + diffOptions := ssa.DiffOptions{ + Exclusions: map[string]string{ + "kustomize.toolkit.fluxcd.io/reconcile": "disabled", + }, + } + change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions) if err != nil { - if b.kustomization.Spec.Force && isImmutableError(err) { + if b.kustomization.Spec.Force && ssa.IsImmutableError(err) { output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green)) } else { - output.WriteString(writeString(fmt.Sprint(`✗`, err), bunt.Red)) + output.WriteString(writeString(fmt.Sprintf("✗ %v\n", err), bunt.Red)) } continue } @@ -283,12 +285,3 @@ 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 -} diff --git a/internal/kustomization/kustomization.go b/internal/kustomization/kustomization.go deleted file mode 100644 index c16afc99..00000000 --- a/internal/kustomization/kustomization.go +++ /dev/null @@ -1,24 +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 - -// SubstituteReference contains a reference to a resource containing -// the variables name and value. -type SubstituteReference struct { - Kind string `json:"kind"` - Name string `json:"name"` -} diff --git a/internal/kustomization/kustomization_generator.go b/internal/kustomization/kustomization_generator.go deleted file mode 100644 index fc70fb8d..00000000 --- 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 f89e3273..00000000 --- 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 - -}