From 862a3b554fe35090e4c0c228b4195e96b3cf4256 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 00:24:26 +0300 Subject: [PATCH 01/40] Add server-side apply utilities (pkg/resmgr) The Resource Manager performs the following actions: - decodes raw manifests (YAML & JSON) into Kubernetes objects - validates the objects with server-side dry-run apply - determines if the in-cluster objects are in drift based on the dry-run result - reconciles the objects on the cluster with server-side apply - waits for the objects to be fully reconciled by looking up their readiness status Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 202 +++++----- cmd/kustomizer/delete.go | 100 ++++- cmd/kustomizer/main.go | 68 +++- cmd/kustomizer/utils.go | 127 ++++++ go.mod | 17 +- go.sum | 844 ++++++++++++++++++++++----------------- pkg/engine/applier.go | 117 ------ pkg/engine/builder.go | 126 ------ pkg/engine/collector.go | 161 -------- pkg/engine/generator.go | 177 -------- pkg/engine/kubectl.go | 66 --- pkg/engine/revisor.go | 62 --- pkg/engine/snapshot.go | 159 -------- pkg/engine/transfomer.go | 63 --- pkg/resmgr/changeset.go | 55 +++ pkg/resmgr/client.go | 108 +++++ pkg/resmgr/doc.go | 26 ++ pkg/resmgr/fmt.go | 53 +++ pkg/resmgr/manager.go | 397 ++++++++++++++++++ pkg/resmgr/sort.go | 72 ++++ 20 files changed, 1568 insertions(+), 1432 deletions(-) create mode 100644 cmd/kustomizer/utils.go delete mode 100644 pkg/engine/applier.go delete mode 100644 pkg/engine/builder.go delete mode 100644 pkg/engine/collector.go delete mode 100644 pkg/engine/generator.go delete mode 100644 pkg/engine/kubectl.go delete mode 100644 pkg/engine/revisor.go delete mode 100644 pkg/engine/snapshot.go delete mode 100644 pkg/engine/transfomer.go create mode 100644 pkg/resmgr/changeset.go create mode 100644 pkg/resmgr/client.go create mode 100644 pkg/resmgr/doc.go create mode 100644 pkg/resmgr/fmt.go create mode 100644 pkg/resmgr/manager.go create mode 100644 pkg/resmgr/sort.go diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index db0585e..03188b3 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -1,139 +1,141 @@ +/* +Copyright 2021 Stefan Prodan +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 ( + "bufio" + "bytes" + "context" "fmt" - "io/ioutil" "os" - "os/exec" - "path/filepath" - "strings" + "sort" "time" "github.com/spf13/cobra" - "sigs.k8s.io/kustomize/api/filesys" - - "github.com/stefanprodan/kustomizer/pkg/engine" + "github.com/stefanprodan/kustomizer/pkg/resmgr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) var applyCmd = &cobra.Command{ - Use: "apply [path]", - Short: "Apply kustomization and prune previous applied Kubernetes objects", - RunE: applyCmdRun, + Use: "apply", + Short: "Apply Kubernetes manifests and Kustomize overlays using server-side apply.", + RunE: runApplyCmd, } -var ( - group string - name string - revision string - timeout time.Duration - cfgNamespace string - buildWithKustomize bool - dryRun bool -) +type applyFlags struct { + filename []string + wait bool + force bool + kustomize string + output string +} + +var applyArgs applyFlags func init() { - applyCmd.Flags().StringVar(&group, "group", "kustomizer", "group") - applyCmd.Flags().StringVarP(&name, "name", "", "", "name of this kustomization") - applyCmd.Flags().StringVarP(&revision, "revision", "r", "", "revision of this kustomization") - applyCmd.Flags().StringVarP(&cfgNamespace, "gc-namespace", "", "default", "namespace to store the GC snapshot ConfigMap") - applyCmd.Flags().DurationVar(&timeout, "timeout", 5*time.Minute, "timeout for this operation") - applyCmd.Flags().BoolVar(&buildWithKustomize, "use-kustomize", false, "use Kustomize binary for build operations") - applyCmd.Flags().BoolVar(&dryRun, "dry-run", false, "dry-run apply") + applyCmd.Flags().StringSliceVarP(&applyArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") + applyCmd.Flags().StringVarP(&applyArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") + applyCmd.Flags().BoolVar(&applyArgs.wait, "wait", false, "wait for the applied Kubernetes objects to become ready") + applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "recreate objects that contain immutable fields changes") + applyCmd.Flags().StringVarP(&applyArgs.output, "output", "o", "", "output can be yaml or json") rootCmd.AddCommand(applyCmd) } -func applyCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("path is required") - } - base := args[0] - fs := filesys.MakeFsOnDisk() - - tmpDir, err := ioutil.TempDir("", name) - if err != nil { - return fmt.Errorf("tmp dir error: %w", err) - } - defer os.RemoveAll(tmpDir) - - if !strings.HasSuffix(base, "/") { - base += "/" - } - - c := fmt.Sprintf("cp -r %s* %s", base, tmpDir) - command := exec.Command("/bin/sh", "-c", c) - if err := command.Run(); err != nil { - return fmt.Errorf("%s command failed", c) - } - - base = tmpDir - - revisor, err := engine.NewRevisior(group, name, revision) - if err != nil { - return err - } - - transformer, err := engine.NewTransformer(fs, revisor) +func runApplyCmd(cmd *cobra.Command, args []string) error { + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "flagger-cli") if err != nil { return err } - err = transformer.Generate(base) - if err != nil { - return err - } - - generator, err := engine.NewGenerator(fs, revisor) - if err != nil { - return err - } + objects := make([]*unstructured.Unstructured, 0) - err = generator.Generate(base) - if err != nil { - return err - } - - builder, err := engine.NewBuilder(fs) - if err != nil { - return err - } - - manifest := filepath.Join(base, revisor.ManifestFile()) - - if buildWithKustomize { - if err = builder.Build(base, manifest); err != nil { + if applyArgs.kustomize != "" { + data, err := buildKustomization(applyArgs.kustomize) + if err != nil { return err } + + objs, err := resMgr.ReadAll(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("%s: %w", applyArgs.kustomize, err) + } + objects = append(objects, objs...) } else { - if err = builder.Generate(base, manifest); err != nil { + manifests, err := scan(applyArgs.filename) + if err != nil { return err } + for _, manifest := range manifests { + ms, err := os.Open(manifest) + if err != nil { + return err + } + + objs, err := resMgr.ReadAll(bufio.NewReader(ms)) + ms.Close() + if err != nil { + return fmt.Errorf("%s: %w", manifest, err) + } + objects = append(objects, objs...) + } } - - applier, err := engine.NewApplier(fs, timeout, engine.NewKubectlExecutor(kubectl, nil)) - if err != nil { - return err - } - - err = applier.Run(manifest, dryRun) - if err != nil { - return err + sort.Sort(resmgr.ApplyOrder(objects)) + + if applyArgs.output != "" { + switch applyArgs.output { + case "yaml": + yml, err := resMgr.ToYAML(objects) + if err != nil { + return err + } + fmt.Println(yml) + return nil + case "json": + json, err := resMgr.ToJSON(objects) + if err != nil { + return err + } + fmt.Println(json) + return nil + default: + return fmt.Errorf("unsupported output, can be yaml or json") + } } - gc, err := engine.NewGarbageCollector(revisor, timeout, engine.NewKubectlExecutor(kubectl, nil)) - if err != nil { - return err - } + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() - write := func(obj string) { - if !strings.Contains(obj, "No resources found") { - fmt.Println(obj) + for _, object := range objects { + change, err := resMgr.Reconcile(ctx, object, applyArgs.force) + if err != nil { + return err } + fmt.Println(change.String()) } - err = gc.Run(manifest, cfgNamespace, write) - if err != nil { - return err + if applyArgs.wait { + fmt.Println("waiting for resources to become ready...") + err = resMgr.Wait(objects, 2*time.Second, rootArgs.timeout) + if err != nil { + return err + } + fmt.Println("all resources are ready") } return nil diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index f15c4f0..0bbf350 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -1,12 +1,34 @@ +/* +Copyright 2021 Stefan Prodan +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 ( + "bufio" + "bytes" + "context" "fmt" - "strings" + "os" + "sort" "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/engine" + "github.com/stefanprodan/kustomizer/pkg/resmgr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) var deleteCmd = &cobra.Command{ @@ -15,41 +37,81 @@ var deleteCmd = &cobra.Command{ RunE: deleteCmdRun, } -var ( - deleteName string - deleteTimeout time.Duration - deleteCfgNamespace string -) +type deleteFlags struct { + filename []string + kustomize string + wait bool +} + +var deleteArgs deleteFlags func init() { - deleteCmd.Flags().StringVarP(&deleteName, "name", "", "", "name of the kustomization to be deleted") - deleteCmd.Flags().StringVarP(&deleteCfgNamespace, "gc-namespace", "", "default", "namespace where the GC snapshot ConfigMap is") - deleteCmd.Flags().DurationVar(&deleteTimeout, "timeout", 5*time.Minute, "timeout for this operation") + deleteCmd.Flags().StringSliceVarP(&deleteArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") + deleteCmd.Flags().StringVarP(&deleteArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") + deleteCmd.Flags().BoolVar(&deleteArgs.wait, "wait", false, "wait for the deleted Kubernetes objects to be terminated") rootCmd.AddCommand(deleteCmd) } func deleteCmdRun(cmd *cobra.Command, args []string) error { - revisor, err := engine.NewRevisior(group, deleteName, "none") + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "flagger-cli") if err != nil { return err } - gc, err := engine.NewGarbageCollector(revisor, deleteTimeout, engine.NewKubectlExecutor(kubectl, nil)) - if err != nil { - return err - } + objects := make([]*unstructured.Unstructured, 0) - write := func(obj string) { - if !strings.Contains(obj, "No resources found") { - fmt.Println(obj) + if applyArgs.kustomize != "" { + data, err := buildKustomization(deleteArgs.kustomize) + if err != nil { + return err + } + + objs, err := resMgr.ReadAll(bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("%s: %w", deleteArgs.kustomize, err) + } + objects = append(objects, objs...) + } else { + manifests, err := scan(deleteArgs.filename) + if err != nil { + return err + } + for _, manifest := range manifests { + ms, err := os.Open(manifest) + if err != nil { + return err + } + + objs, err := resMgr.ReadAll(bufio.NewReader(ms)) + ms.Close() + if err != nil { + return fmt.Errorf("%s: %w", manifest, err) + } + objects = append(objects, objs...) } } + sort.Sort(resmgr.ApplyOrder(objects)) - err = gc.Cleanup(deleteCfgNamespace, write) + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + changeSet, err := resMgr.DeleteAll(ctx, objects) if err != nil { return err } + for _, change := range changeSet.Entries { + fmt.Println(change.String()) + } + + if deleteArgs.wait { + fmt.Println("waiting for resources to be terminated...") + err = resMgr.WaitForTermination(objects, 2*time.Second, rootArgs.timeout) + if err != nil { + return err + } + fmt.Println("all resources have been deleted") + } return nil } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index d69bdf0..2569096 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -1,10 +1,31 @@ +/* +Copyright 2021 Stefan Prodan +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 ( - "log" + "flag" "os" + "path/filepath" + "time" "github.com/spf13/cobra" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/klog/v2" ) var VERSION = "0.0.0-dev.0" @@ -14,21 +35,52 @@ var rootCmd = &cobra.Command{ Version: VERSION, SilenceUsage: true, SilenceErrors: true, - Short: "A command line utility for generating, building and applying kustomizations", + Short: "A command line utility for reconciling Kubernetes manifests and Kustomize overlays.", } -var ( - kubectl string -) +type rootFlags struct { + kubeconfig string + kubecontext string + timeout time.Duration +} + +var rootArgs = rootFlags{} func init() { - rootCmd.PersistentFlags().StringVar(&kubectl, "kubectl", "kubectl", "Command to run kubectl") + rootCmd.PersistentFlags().StringVarP(&rootArgs.kubeconfig, "kubeconfig", "", "", + "absolute path to the kubeconfig file") + rootCmd.PersistentFlags().StringVarP(&rootArgs.kubecontext, "context", "", "", "kubernetes context to use") + rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", time.Minute, "timeout for this operation") + + rootCmd.DisableAutoGenTag = true } func main() { - log.SetFlags(0) + klog.InitFlags(nil) + flag.Parse() + + configureKubeconfig() if err := rootCmd.Execute(); err != nil { - log.Println(err) + klog.Errorf("%v", err) os.Exit(1) } } + +func configureKubeconfig() { + switch { + case len(rootArgs.kubeconfig) > 0: + case len(os.Getenv("KUBECONFIG")) > 0: + rootArgs.kubeconfig = os.Getenv("KUBECONFIG") + default: + if home := homeDir(); len(home) > 0 { + rootArgs.kubeconfig = filepath.Join(home, ".kube", "config") + } + } +} + +func homeDir() string { + if h := os.Getenv("HOME"); h != "" { + return h + } + return os.Getenv("USERPROFILE") // windows +} diff --git a/cmd/kustomizer/utils.go b/cmd/kustomizer/utils.go new file mode 100644 index 0000000..9cd04e7 --- /dev/null +++ b/cmd/kustomizer/utils.go @@ -0,0 +1,127 @@ +/* +Copyright 2021 Stefan Prodan +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" + "path" + "path/filepath" + "sync" + + "sigs.k8s.io/kustomize/api/filesys" + "sigs.k8s.io/kustomize/api/krusty" + kustypes "sigs.k8s.io/kustomize/api/types" +) + +func scan(paths []string) ([]string, error) { + var manifests []string + + for _, in := range paths { + fi, err := os.Stat(in) + if err != nil { + return nil, err + } + + switch mode := fi.Mode(); { + case mode.IsDir(): + m, err := scanRec(in) + if err != nil { + return nil, err + } + manifests = append(manifests, m...) + case mode.IsRegular(): + if matchExt(fi.Name()) { + manifests = append(manifests, in) + } + } + } + + return manifests, nil +} + +func scanRec(dir string) ([]string, error) { + var manifests []string + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + for _, file := range files { + if file.IsDir() { + m, err := scanRec(path.Join(dir, file.Name())) + if err != nil { + return nil, err + } + manifests = append(manifests, m...) + } + if matchExt(file.Name()) { + manifests = append(manifests, path.Join(dir, file.Name())) + } + } + return manifests, err +} + +func matchExt(f string) bool { + ext := path.Ext(f) + return ext == ".yaml" || ext == ".yml" +} + +var kustomizeBuildMutex sync.Mutex + +func buildKustomization(base string) ([]byte, error) { + kustomizeBuildMutex.Lock() + defer kustomizeBuildMutex.Unlock() + + kfile := path.Join(base, "kustomization.yaml") + + fs := filesys.MakeFsOnDisk() + if !fs.Exists(kfile) { + return nil, fmt.Errorf("%s not found", kfile) + } + + if path.IsAbs(base) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + base, err = filepath.Rel(wd, base) + if err != nil { + return nil, err + } + } + + buildOptions := &krusty.Options{ + LoadRestrictions: kustypes.LoadRestrictionsNone, + AddManagedbyLabel: false, + DoPrune: false, + PluginConfig: kustypes.DisabledPluginConfig(), + } + + k := krusty.MakeKustomizer(buildOptions) + m, err := k.Run(fs, base) + if err != nil { + return nil, err + } + + resources, err := m.AsYaml() + if err != nil { + return nil, err + } + + return resources, nil +} diff --git a/go.mod b/go.mod index 8340a54..129acce 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,16 @@ module github.com/stefanprodan/kustomizer -go 1.14 +go 1.16 require ( - github.com/spf13/cobra v1.0.0 - go.mozilla.org/sops/v3 v3.6.0 - k8s.io/api v0.19.0 - k8s.io/apimachinery v0.19.0 - k8s.io/client-go v0.19.0 // indirect - sigs.k8s.io/kustomize/api v0.6.0 + github.com/spf13/cobra v1.1.3 + k8s.io/api v0.21.3 + k8s.io/apiextensions-apiserver v0.21.3 + k8s.io/apimachinery v0.21.3 + k8s.io/client-go v0.21.3 + k8s.io/klog/v2 v2.9.0 + sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a + sigs.k8s.io/controller-runtime v0.9.6 + sigs.k8s.io/kustomize/api v0.9.0 sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 0a6e309..028fe58 100644 --- a/go.sum +++ b/go.sum @@ -1,171 +1,173 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.43.0 h1:banaiRPAM8kUVYneOSkhgcDsLzEvL25FinuiSZaH/2w= -cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM= -cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE= -github.com/Azure/azure-sdk-for-go v31.2.0+incompatible h1:kZFnTLmdQYNGfakatSivKHUfUnDZhqNdchHD4oIhp5k= -github.com/Azure/azure-sdk-for-go v31.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0= -github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= -github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0= -github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= -github.com/Azure/go-autorest/autorest/azure/auth v0.1.0 h1:YgO/vSnJEc76NLw2ecIXvXa8bDWiqf1pOJzARAoZsYU= -github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= -github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 h1:YTtBrcb6mhA+PoSW8WxFDoIIyjp13XqJeX80ssQtri4= -github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= -github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0 h1:Ww5g4zThfD/6cLb4z6xxgeyDa7QDkizMkJKe0ysZXp0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= -github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= -github.com/Azure/go-autorest/autorest/to v0.3.0 h1:zebkZaadz7+wIQYgC7GXaz3Wb28yKYfVkkBKwc38VF8= -github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= -github.com/Azure/go-autorest/autorest/validation v0.2.0 h1:15vMO4y76dehZSq7pAaOLQxC6dZYsSrj2GQpflyM/L4= -github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +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/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM= -github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.23.13 h1:l/NG+mgQFRGG3dsFzEj0jw9JIs/zYdtU6MXhY1WIDmM= -github.com/aws/aws-sdk-go v1.23.13/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 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/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk= +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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-critic/go-critic v0.3.5-0.20190904082202-d79a9f0c64db/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM= +github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= -github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= +github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= @@ -174,13 +176,11 @@ github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= @@ -194,19 +194,16 @@ github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCs github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -215,78 +212,56 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= -github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= -github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= -github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= -github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= -github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= -github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= -github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= -github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= -github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= -github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= -github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= -github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= -github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= -github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= -github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= -github.com/golangci/golangci-lint v1.21.0/go.mod h1:phxpHK52q7SE+5KpPnti4oZTdFCEsn/tKN+nFvCKXfk= -github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= -github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= -github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= -github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= -github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= -github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= -github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= 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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -294,324 +269,356 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible h1:AQwinXlbQR2HvPjQZOmDhRqsv5mZf+Jb1RnSLxcqZcI= -github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= -github.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw= -github.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= -github.com/hashicorp/go-retryablehttp v0.5.4 h1:1BZvpawXoJCWX6pNtow9+rpEj+3itIlutiqnntI6jOE= -github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= -github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +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= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v1.0.4 h1:j08Or/wryXT4AcHj1oCbMd7IijXcKzYUGw59LGu9onU= -github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= -github.com/hashicorp/vault/sdk v0.1.13 h1:mOEPeOhT7jl0J4AMl1E705+BcmeRs1VmKNb9F0sMLy8= -github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= -github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= 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-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= 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/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +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 v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c h1:yE1NxRAZA3wF0laDWECtOe2J0tFjSHUI6MXXbMif+QY= -github.com/mozilla-services/yaml v0.0.0-20191106225358-5c216288813c/go.mod h1:Is/Ucts/yU/mWyGR8yELRoO46mejouKsJfQLAIfTR18= -github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= +github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8= -github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/paulmach/orb v0.1.3/go.mod h1:VFlX/8C+IQ1p6FTRRKzKoOPJnvEtA5G0Veuqwbu//Vk= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/qri-io/starlib v0.4.2-0.20200213133954-ff2e8cd5ef8d h1:K6eOUihrFLdZjZnA4XlRp864fmWXv9YTIk7VPLhRacA= -github.com/qri-io/starlib v0.4.2-0.20200213133954-ff2e8cd5ef8d/go.mod h1:7DPO4domFU579Ga6E61sB9VFNaniPVwJP5C4bBCu3wA= -github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/securego/gosec v0.0.0-20191002120514-e680875ea14d/go.mod h1:w5+eXa0mYznDkHaMCXA4XYffjlH+cy1oyKbfzJXa2Do= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 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/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 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= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI= -github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= +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/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= -github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= -github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= -github.com/uudashr/gocognit v0.0.0-20190926065955-1655d0de0517/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= -github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yujunz/go-getter v1.4.1-lite h1:FhvNc94AXMZkfqUwfMKhnQEC9phkphSGdPTL7tIdhOM= -github.com/yujunz/go-getter v1.4.1-lite/go.mod h1:sbmqxXjyLunH1PkF3n7zSlnVeMvmYUuIl9ZVs/7NyCc= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a h1:N7VD+PwpJME2ZfQT8+ejxwA4Ow10IkGbU0MGf94ll8k= -go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a/go.mod h1:YDKUvO0b//78PaaEro6CAPH6NqohCmL2Cwju5XI2HoE= -go.mozilla.org/sops/v3 v3.6.0 h1:V+RjhX96enZY9a5iVP/r60lLABq8/8Pv2Fybh10Np3g= -go.mozilla.org/sops/v3 v3.6.0/go.mod h1:X8YOCEZzMFL0p28vkqtn3gW2eFt+dDUt7HwKXGvcXvA= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.starlark.net v0.0.0-20190528202925-30ae18b8564f/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -620,20 +627,26 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -648,41 +661,47 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/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-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/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= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -690,107 +709,165 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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 h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -799,82 +876,115 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0= -gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo= -gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= -k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= -k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= -k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ= -k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= -k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg= -k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= -k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k= -k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= +k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ= +k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= +k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= +k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY= +k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= +k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= +k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= +k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= +k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= +k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= +k8s.io/cli-runtime v0.21.1 h1:Oj/iZxa7LLXrhzShaLNF4rFJEIEBTDHj0dJw4ra2vX4= +k8s.io/cli-runtime v0.21.1/go.mod h1:TI9Bvl8lQWZB2KqE91QLCp9AZE4l29zNFnj/x4IX4Fw= +k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= +k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg= +k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= +k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= +k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= +k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= +k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6 h1:+WnxoVtG8TMiudHBSEtrVL1egv36TkkJm+bA8AxicmQ= -k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= -k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= -mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= -mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= +k8s.io/kubectl v0.21.1/go.mod h1:PMYR88MqESuysBM/MX+Vu4JbX/50nY4d4kny+SPEI2U= +k8s.io/metrics v0.21.1/go.mod h1:pyDVLsLe++FIGDBFU80NcW4xMFsuiVTWL8Zfi7+PpNo= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210517184530-5a248b5acedc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471 h1:DnzUXII7sVg1FJ/4JX6YDRJfLNAC7idRatPwe07suiI= +k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/kustomize/api v0.6.0 h1:Gj+MH9uEPh7tBHKCGGwA+fHgg9th55StaU+ZT05+8bY= -sigs.k8s.io/kustomize/api v0.6.0/go.mod h1:M7410E0ULUFQlxRskB//n5G0MPwGvs9HG6K8Sf8gw+M= -sigs.k8s.io/kustomize/kyaml v0.7.1 h1:Ih6SJPvfKYfZaIFWUa2YAyg/0ZSTpA3LFjR/hv7+8ao= -sigs.k8s.io/kustomize/kyaml v0.7.1/go.mod h1:ne3F9JPhW2wrVaLslxBsEe6MQJQ9YK5rUutrdhBWXwI= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a h1:S17+FPWGsOonXim+GcadLcSblEwL131Y9fKLfifSYkY= +sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a/go.mod h1:I4jgHr6uRfX0CkOMECwSgg2J48rNzZE1+kDXj9SnJBc= +sigs.k8s.io/controller-runtime v0.9.0-beta.5.0.20210524185538-7181f1162e79/go.mod h1:rgf+cBz72pYlKXDRNhI1WFQv/S86EMUV4/ySmsEYgHk= +sigs.k8s.io/controller-runtime v0.9.6 h1:EevVMlgUj4fC1NVM4+DB3iPkWkmGRNarA66neqv9Qew= +sigs.k8s.io/controller-runtime v0.9.6/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= +sigs.k8s.io/kustomize/api v0.8.8/go.mod h1:He1zoK0nk43Pc6NlV085xDXDXTNprtcyKZVm3swsdNY= +sigs.k8s.io/kustomize/api v0.9.0 h1:yGQnm/2GBbHBLSVbM0CsqgPmqK/NSDbhSGbXuhuVN7s= +sigs.k8s.io/kustomize/api v0.9.0/go.mod h1:bOF7z4DcRIXcOCeSbVq5o9JhMRnNzWqrRSSBFtz05A4= +sigs.k8s.io/kustomize/cmd/config v0.9.10/go.mod h1:Mrby0WnRH7hA6OwOYnYpfpiY0WJIMgYrEDfwOeFdMK0= +sigs.k8s.io/kustomize/kustomize/v4 v4.1.2/go.mod h1:PxBvo4WGYlCLeRPL+ziT64wBXqbgfcalOS/SXa/tcyo= +sigs.k8s.io/kustomize/kyaml v0.10.17/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg= +sigs.k8s.io/kustomize/kyaml v0.11.1 h1:MWihd9syKG7VQnAzr/OpKb94FvH+cw96nEV1u3it7pI= +sigs.k8s.io/kustomize/kyaml v0.11.1/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= +sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pkg/engine/applier.go b/pkg/engine/applier.go deleted file mode 100644 index bae6324..0000000 --- a/pkg/engine/applier.go +++ /dev/null @@ -1,117 +0,0 @@ -package engine - -import ( - "bytes" - "context" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "time" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/yaml" - "sigs.k8s.io/kustomize/api/filesys" - yaml2 "sigs.k8s.io/yaml" -) - -type Applier struct { - fs filesys.FileSystem - timeout time.Duration - kubectl KubectlExecutor -} - -func NewApplier(fs filesys.FileSystem, timeout time.Duration, ke KubectlExecutor) (*Applier, error) { - return &Applier{ - fs: fs, - timeout: timeout, - kubectl: ke, - }, nil -} - -func (a *Applier) Run(manifestPath string, dryRun bool) error { - if !a.fs.Exists(manifestPath) { - return fmt.Errorf("%s not found", manifestPath) - } - - crds, err := a.ExtractCRDs(manifestPath) - if err != nil { - return err - } - - ctx, cancel := context.WithTimeout(context.Background(), a.timeout+time.Second) - defer cancel() - - if crds != "" { - args := []string{"apply", "-f", crds, "--timeout", a.timeout.String()} - if dryRun { - args = append(args, "--dry-run", "client") - } - - if err := a.kubectl.Exec(ctx, args...); err != nil { - return err - } - } - - args := []string{"apply", "-f", manifestPath, "--timeout", a.timeout.String()} - if dryRun { - args = append(args, "--dry-run", "client") - } - - return a.kubectl.Exec(ctx, args...) -} - -func (a *Applier) ExtractCRDs(manifestPath string) (string, error) { - manifests, err := ioutil.ReadFile(manifestPath) - if err != nil { - return "", err - } - - crds := "" - reader := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifests), 2048) - for { - var obj unstructured.Unstructured - err := reader.Decode(&obj) - if err == io.EOF { - break - } else if err != nil { - return "", err - } - if obj.IsList() { - err := obj.EachListItem(func(item runtime.Object) error { - return nil - }) - if err != nil { - return "", err - } - } else { - if obj.GetKind() == "CustomResourceDefinition" { - b, err := obj.MarshalJSON() - if err != nil { - return "", err - } - - y, err := yaml2.JSONToYAML(b) - if err != nil { - return "", err - } - crds += "---\n" + string(y) - } - } - } - - if crds == "" { - return "", nil - } - - base := filepath.Dir(manifestPath) - crdsFile := filepath.Join(base, "extracted-crds.yaml") - - if err := ioutil.WriteFile(crdsFile, []byte(crds), os.ModePerm); err != nil { - return "", err - } - - return crdsFile, nil -} diff --git a/pkg/engine/builder.go b/pkg/engine/builder.go deleted file mode 100644 index c5605f6..0000000 --- a/pkg/engine/builder.go +++ /dev/null @@ -1,126 +0,0 @@ -package engine - -import ( - "bytes" - "fmt" - "os/exec" - "path/filepath" - "runtime" - - "go.mozilla.org/sops/v3/aes" - "go.mozilla.org/sops/v3/cmd/sops/common" - "go.mozilla.org/sops/v3/cmd/sops/formats" - "sigs.k8s.io/kustomize/api/filesys" - "sigs.k8s.io/kustomize/api/krusty" - "sigs.k8s.io/kustomize/api/resource" - "sigs.k8s.io/yaml" -) - -type Builder struct { - fs filesys.FileSystem -} - -func NewBuilder(fs filesys.FileSystem) (*Builder, error) { - return &Builder{fs: fs}, nil -} - -// Generate Kubernetes manifests from a kustomization -func (b *Builder) Generate(base string, filePath string) error { - kfile := filepath.Join(base, "kustomization.yaml") - - if !b.fs.Exists(kfile) { - return fmt.Errorf("%s not found", kfile) - } - - opt := krusty.MakeDefaultOptions() - opt.DoLegacyResourceSort = true - k := krusty.MakeKustomizer(b.fs, opt) - m, err := k.Run(base) - if err != nil { - return err - } - - // check if resources are SOPS encrypted and decrypt them before - // generating the final YAML - for _, res := range m.Resources() { - outRes, err := b.decryptSOPS(res) - if err != nil { - return err - } - - if outRes != nil { - _, err = m.Replace(res) - if err != nil { - return err - } - } - } - - resources, err := m.AsYaml() - if err != nil { - return err - } - - if err := b.fs.WriteFile(filePath, resources); err != nil { - return err - } - - return nil -} - -func (b *Builder) decryptSOPS(res *resource.Resource) (*resource.Resource, error) { - out, err := res.AsYAML() - if err != nil { - return nil, err - } - - if bytes.Contains(out, []byte("sops:")) && bytes.Contains(out, []byte("mac: ENC[")) { - store := common.StoreForFormat(formats.Yaml) - - // Load SOPS file and access the data key - tree, err := store.LoadEncryptedFile(out) - if err != nil { - return nil, fmt.Errorf("LoadEncryptedFile: %w", err) - } - key, err := tree.Metadata.GetDataKey() - if err != nil { - return nil, fmt.Errorf("GetDataKey: %w", err) - } - - // Decrypt the tree - cipher := aes.NewCipher() - if _, err := tree.Decrypt(key, cipher); err != nil { - return nil, fmt.Errorf("Decrypt: %w", err) - } - - data, err := store.EmitPlainFile(tree.Branches) - if err != nil { - return nil, fmt.Errorf("EmitPlainFile: %w", err) - } - - jsonData, err := yaml.YAMLToJSON(data) - if err != nil { - return nil, fmt.Errorf("YAMLToJSON: %w", err) - } - - err = res.UnmarshalJSON(jsonData) - if err != nil { - return nil, fmt.Errorf("UnmarshalJSON: %w", err) - } - return res, nil - } - return nil, nil -} - -func (b *Builder) Build(base string, filePath string) error { - if _, err := exec.LookPath("kustomize"); err != nil { - return fmt.Errorf("kustomize not found") - } - - command := fmt.Sprintf("kustomize build %s > %s", base, filePath) - c := exec.Command("/bin/sh", "-c", command) - if runtime.GOOS == "windows" { - c = exec.Command("cmd", "/c", command) - } - return c.Run() -} diff --git a/pkg/engine/collector.go b/pkg/engine/collector.go deleted file mode 100644 index 8b89f2d..0000000 --- a/pkg/engine/collector.go +++ /dev/null @@ -1,161 +0,0 @@ -package engine - -import ( - "context" - "fmt" - "io/ioutil" - "os/exec" - "strings" - "time" -) - -type GarbageCollector struct { - rv *Revisor - timeout time.Duration - kubectl KubectlExecutor -} - -func NewGarbageCollector(revisor *Revisor, timeout time.Duration, ke KubectlExecutor) (*GarbageCollector, error) { - if revisor == nil { - return nil, fmt.Errorf("revisor is nil") - } - - if _, err := exec.LookPath("kubectl"); err != nil { - return nil, fmt.Errorf("kubectl not found") - } - - return &GarbageCollector{ - rv: revisor, - timeout: timeout, - kubectl: ke, - }, nil -} - -func (gc *GarbageCollector) Run(manifestsFile string, cfgNamespace string, write func(string)) error { - data, err := ioutil.ReadFile(manifestsFile) - if err != nil { - return err - } - - newSnapshot, err := NewSnapshot(data, gc.rv.revision) - if err != nil { - return err - } - - firstTime := false - cfg, err := gc.getSnapshot(cfgNamespace) - if err != nil { - if strings.Contains(err.Error(), "NotFound") { - firstTime = true - } else { - return err - } - } - - if !firstTime { - oldSnapshot, err := NewSnapshotFromConfigMap(cfg) - if err != nil { - return err - } - - if newSnapshot.Revision != oldSnapshot.Revision { - err := gc.prune(*oldSnapshot, false, write) - if err != nil { - return err - } - } - } - - newCfg, err := newSnapshot.ToConfigMap(gc.rv.SnapshotName(), cfgNamespace) - if err != nil { - return err - } - - msg, err := gc.applySnapshot(newCfg) - if err != nil { - return err - } - - write(msg) - return nil -} - -func (gc *GarbageCollector) Cleanup(cfgNamespace string, write func(string)) error { - cfg, err := gc.getSnapshot(cfgNamespace) - if err != nil { - return err - } - - snapshot, err := NewSnapshotFromConfigMap(cfg) - if err != nil { - return err - } - - err = gc.prune(*snapshot, false, write) - if err != nil { - return err - } - - msg, err := gc.deleteSnapshot(cfgNamespace) - if err != nil { - return err - } - - write(msg) - - return nil -} - -func (gc *GarbageCollector) prune(snapshot Snapshot, dryRun bool, write func(string)) error { - selector := gc.rv.PrevSelectors(snapshot.Revision) - for ns, kinds := range snapshot.NamespacedKinds() { - for _, kind := range kinds { - if output, err := gc.deleteByKind(kind, ns, selector, dryRun, gc.timeout); err != nil { - write(err.Error()) - } else { - write(output) - } - } - } - - for _, kind := range snapshot.NonNamespacedKinds() { - if output, err := gc.deleteByKind(kind, "", selector, dryRun, gc.timeout); err != nil { - write(err.Error()) - } else { - write(output) - } - } - - return nil -} - -func (gc *GarbageCollector) deleteByKind(kind string, namespace string, selector string, dryRun bool, timeout time.Duration) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), timeout+time.Second) - defer cancel() - - args := []string{"delete", kind, "-l", selector} - - if namespace != "" { - args = append(args, "-n", namespace) - } - if dryRun { - args = append(args, "--dry-run", "server") - } - - return gc.kubectl.Get(ctx, args...) -} - -func (gc *GarbageCollector) getSnapshot(cfgNamespace string) (string, error) { - args := []string{"-n", cfgNamespace, "get", "configmap", gc.rv.SnapshotName(), "-o", "yaml"} - return gc.kubectl.Get(context.TODO(), args...) -} - -func (gc *GarbageCollector) applySnapshot(cfg string) (string, error) { - args := []string{"apply", "-f", "-"} - return gc.kubectl.Pipe(context.TODO(), cfg, args...) -} - -func (gc *GarbageCollector) deleteSnapshot(cfgNamespace string) (string, error) { - args := []string{"-n", cfgNamespace, "delete", "configmap", gc.rv.SnapshotName()} - return gc.kubectl.Get(context.TODO(), args...) -} diff --git a/pkg/engine/generator.go b/pkg/engine/generator.go deleted file mode 100644 index a5d2e0e..0000000 --- a/pkg/engine/generator.go +++ /dev/null @@ -1,177 +0,0 @@ -package engine - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "sigs.k8s.io/kustomize/api/filesys" - "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" - "sigs.k8s.io/kustomize/api/konfig" - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" -) - -type Generator struct { - rv *Revisor - fs filesys.FileSystem -} - -func NewGenerator(fs filesys.FileSystem, revisor *Revisor) (*Generator, error) { - if revisor == nil { - return nil, fmt.Errorf("revisor is nil") - } - - return &Generator{ - rv: revisor, - fs: fs, - }, nil -} - -// Generate kustomization file or append label transformer to an existing one -func (g *Generator) Generate(base string) error { - kfile := filepath.Join(base, "kustomization.yaml") - if g.fs.Exists(kfile) { - if err := g.edit(base); err != nil { - return err - } - } else { - if err := g.create(base); err != nil { - return err - } - } - return nil -} - -func (g *Generator) create(base string) error { - kfile := filepath.Join(base, "kustomization.yaml") - - path, err := filepath.Abs(base) - if err != nil { - return err - } - - files, err := g.scan(path, true) - if err != nil { - return err - } - - f, err := g.fs.Create(kfile) - if err != nil { - return err - } - f.Close() - - kustomization := types.Kustomization{ - TypeMeta: types.TypeMeta{ - APIVersion: types.KustomizationVersion, - Kind: types.KustomizationKind, - }, - } - - var resources []string - - for _, file := range files { - if _, name := filepath.Split(file); name == g.rv.LabelsFile() { - continue - } - resources = append(resources, strings.Replace(file, path, ".", 1)) - } - - kustomization.Resources = resources - kustomization.Transformers = []string{g.rv.LabelsFile()} - - data, err := yaml.Marshal(kustomization) - if err != nil { - return err - } - - if err := g.fs.WriteFile(kfile, data); err != nil { - return err - } - return nil -} - -func (g *Generator) scan(base string, recursive bool) ([]string, error) { - var paths []string - uf := kunstruct.NewKunstructuredFactoryImpl() - err := g.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 !recursive { - return filepath.SkipDir - } - // 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 g.fs.Exists(filepath.Join(path, kfilename)) { - paths = append(paths, path) - return filepath.SkipDir - } - } - return nil - } - fContents, err := g.fs.ReadFile(path) - if err != nil { - return err - } - if _, err := uf.SliceFromBytes(fContents); err != nil { - return nil - } - paths = append(paths, path) - return nil - }) - return paths, err -} - -func (g *Generator) edit(base string) error { - kfile := filepath.Join(base, "kustomization.yaml") - - f, err := g.fs.ReadFile(kfile) - if err != nil { - return err - } - - kustomization := types.Kustomization{ - TypeMeta: types.TypeMeta{ - APIVersion: types.KustomizationVersion, - Kind: types.KustomizationKind, - }, - } - - if err := yaml.Unmarshal(f, &kustomization); err != nil { - return err - } - - if len(kustomization.Transformers) == 0 { - kustomization.Transformers = []string{g.rv.LabelsFile()} - } else { - var exists bool - for _, transformer := range kustomization.Transformers { - if transformer == g.rv.LabelsFile() { - exists = true - break - } - } - if !exists { - kustomization.Transformers = append(kustomization.Transformers, g.rv.LabelsFile()) - } - } - - data, err := yaml.Marshal(kustomization) - if err != nil { - return err - } - - if err := g.fs.WriteFile(kfile, data); err != nil { - return err - } - - return nil -} diff --git a/pkg/engine/kubectl.go b/pkg/engine/kubectl.go deleted file mode 100644 index bd2283e..0000000 --- a/pkg/engine/kubectl.go +++ /dev/null @@ -1,66 +0,0 @@ -package engine - -import ( - "context" - "fmt" - "os" - "os/exec" - "strings" -) - -// KubectlExecutor an executor that shells out to run commands -type KubectlExecutor struct { - kubectl string - envVars []string -} - -// NewKubectlExecutor creates a new executor that runs kubectl commands -func NewKubectlExecutor(kubectl string, envVars []string) KubectlExecutor { - return KubectlExecutor{ - envVars: envVars, - kubectl: kubectl, - } -} - -// Exec execute the kubectl command with the specified args -func (e KubectlExecutor) Exec(ctx context.Context, args ...string) error { - cmd := e.buildCmd(ctx, args) - if len(e.envVars) > 0 { - cmd.Env = e.envVars - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -// Get execute the kubectl command with the specified args and returns the output as string -func (e KubectlExecutor) Get(ctx context.Context, args ...string) (string, error) { - cmd := e.buildCmd(ctx, args) - if len(e.envVars) > 0 { - cmd.Env = e.envVars - } - if output, err := cmd.CombinedOutput(); err != nil { - return "", fmt.Errorf("%s", string(output)) - } else { - return strings.TrimSuffix(string(output), "\n"), nil - } -} - -// Pipe execute the kubectl command by piping the yaml arg -func (e KubectlExecutor) Pipe(ctx context.Context, yaml string, args ...string) (string, error) { - cmd := e.buildCmd(ctx, args) - if len(e.envVars) > 0 { - cmd.Env = e.envVars - } - cmd.Stdin = strings.NewReader(yaml) - if output, err := cmd.CombinedOutput(); err != nil { - return "", fmt.Errorf("%s", string(output)) - } else { - return strings.TrimSuffix(string(output), "\n"), nil - } -} - -func (e KubectlExecutor) buildCmd(ctx context.Context, args []string) *exec.Cmd { - s := append(strings.Fields(e.kubectl), args...) - return exec.CommandContext(ctx, s[0], s[1:]...) -} diff --git a/pkg/engine/revisor.go b/pkg/engine/revisor.go deleted file mode 100644 index 3ad042b..0000000 --- a/pkg/engine/revisor.go +++ /dev/null @@ -1,62 +0,0 @@ -package engine - -import ( - "crypto/sha1" - "fmt" -) - -type Revisor struct { - group string - name string - revision string -} - -func NewRevisior(group, name, revision string) (*Revisor, error) { - if group == "" { - return nil, fmt.Errorf("group not specified") - } - if name == "" { - return nil, fmt.Errorf("name not specified") - } - if revision == "" { - return nil, fmt.Errorf("revision not specified") - } - - return &Revisor{ - group: group, - name: name, - revision: revision, - }, nil -} - -func (r *Revisor) Hash() string { - gv := fmt.Sprintf("%s-%s", r.group, r.name) - return fmt.Sprintf("%x", sha1.Sum([]byte(gv))) -} - -func (r *Revisor) Labels() map[string]string { - return map[string]string{ - fmt.Sprintf("%s/name", r.group): r.name, - fmt.Sprintf("%s/revision", r.group): r.revision, - } -} - -func (r *Revisor) LabelsFile() string { - return fmt.Sprintf("%s-labels.yaml", r.Hash()) -} - -func (r *Revisor) NextSelectors() string { - return fmt.Sprintf("%s/name=%s,%s/revision=%s", r.group, r.name, r.group, r.revision) -} - -func (r *Revisor) PrevSelectors(revision string) string { - return fmt.Sprintf("%s/name=%s,%s/revision=%s", r.group, r.name, r.group, revision) -} - -func (r *Revisor) ManifestFile() string { - return fmt.Sprintf("%s-manifest.yaml", r.Hash()) -} - -func (r *Revisor) SnapshotName() string { - return fmt.Sprintf("%s-snapshot", r.name) -} diff --git a/pkg/engine/snapshot.go b/pkg/engine/snapshot.go deleted file mode 100644 index a62bf2c..0000000 --- a/pkg/engine/snapshot.go +++ /dev/null @@ -1,159 +0,0 @@ -package engine - -import ( - "bytes" - js "encoding/json" - "fmt" - "io" - - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer/json" - "k8s.io/apimachinery/pkg/util/yaml" -) - -type Snapshot struct { - Revision string `json:"revision"` - Entries []SnapshotEntry `json:"entries"` -} - -type SnapshotEntry struct { - Namespace string `json:"namespace"` - Kinds map[string]string `json:"kinds"` -} - -func NewSnapshot(manifests []byte, revision string) (*Snapshot, error) { - snapshot := Snapshot{ - Revision: revision, - Entries: []SnapshotEntry{}, - } - - reader := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(manifests), 2048) - for { - var obj unstructured.Unstructured - err := reader.Decode(&obj) - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - if obj.IsList() { - err := obj.EachListItem(func(item runtime.Object) error { - snapshot.addEntry(item.(*unstructured.Unstructured)) - return nil - }) - if err != nil { - return nil, err - } - } else { - snapshot.addEntry(&obj) - } - } - - return &snapshot, nil -} - -func NewSnapshotFromConfigMap(manifest string) (*Snapshot, error) { - reader := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(manifest), 2048) - var cm corev1.ConfigMap - err := reader.Decode(&cm) - if err != nil { - return nil, err - } - - if _, ok := cm.Data["snapshot"]; !ok { - return nil, fmt.Errorf("snapshot data not found") - } - - data := []byte(cm.Data["snapshot"]) - - var snapshot Snapshot - err = js.Unmarshal(data, &snapshot) - if err != nil { - return nil, err - } - - return &snapshot, err -} - -func (s *Snapshot) ToConfigMap(name, namespace string) (string, error) { - data, err := js.Marshal(s) - if err != nil { - return "", err - } - cm := &corev1.ConfigMap{ - TypeMeta: v1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Data: map[string]string{ - "snapshot": string(data), - }, - } - - scheme := runtime.NewScheme() - serializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{ - Pretty: false, - Yaml: false, - Strict: true, - }) - - buf := bytes.NewBufferString("") - err = serializer.Encode(cm, buf) - if err != nil { - return "", err - } - - return buf.String(), nil -} - -func (s *Snapshot) addEntry(item *unstructured.Unstructured) { - found := false - for _, tracker := range s.Entries { - if tracker.Namespace == item.GetNamespace() { - tracker.Kinds[item.GetKind()] = item.GetAPIVersion() - found = true - break - } - } - if !found { - s.Entries = append(s.Entries, SnapshotEntry{ - Namespace: item.GetNamespace(), - Kinds: map[string]string{ - item.GetKind(): item.GetAPIVersion(), - }, - }) - } -} - -func (s *Snapshot) NonNamespacedKinds() []string { - kinds := make([]string, 0) - for _, tracker := range s.Entries { - if tracker.Namespace == "" { - for k, _ := range tracker.Kinds { - kinds = append(kinds, k) - } - } - } - return kinds -} - -func (s *Snapshot) NamespacedKinds() map[string][]string { - nsk := make(map[string][]string) - for _, tracker := range s.Entries { - if tracker.Namespace != "" { - var kinds []string - for k, _ := range tracker.Kinds { - kinds = append(kinds, k) - } - nsk[tracker.Namespace] = kinds - } - } - return nsk -} diff --git a/pkg/engine/transfomer.go b/pkg/engine/transfomer.go deleted file mode 100644 index f8e0faf..0000000 --- a/pkg/engine/transfomer.go +++ /dev/null @@ -1,63 +0,0 @@ -package engine - -import ( - "fmt" - "path/filepath" - - "sigs.k8s.io/kustomize/api/filesys" - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" -) - -type Transformer struct { - rv *Revisor - fs filesys.FileSystem -} - -func NewTransformer(fs filesys.FileSystem, revisor *Revisor) (*Transformer, error) { - if revisor == nil { - return nil, fmt.Errorf("revisor is nil") - } - - return &Transformer{ - rv: revisor, - fs: fs, - }, nil -} - -// Generate label transformer file in the base dir -func (t *Transformer) Generate(base string) error { - var lt = struct { - ApiVersion string `json:"apiVersion" yaml:"apiVersion"` - Kind string `json:"kind" yaml:"kind"` - Metadata struct { - Name string `json:"name" yaml:"name"` - } `json:"metadata" yaml:"metadata"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - FieldSpecs []types.FieldSpec `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"` - }{ - ApiVersion: "builtin", - Kind: "LabelTransformer", - Metadata: struct { - Name string `json:"name" yaml:"name"` - }{ - Name: t.rv.name, - }, - Labels: t.rv.Labels(), - FieldSpecs: []types.FieldSpec{ - {Path: "metadata/labels", CreateIfNotPresent: true}, - }, - } - - data, err := yaml.Marshal(lt) - if err != nil { - return err - } - - path := filepath.Join(base, t.rv.LabelsFile()) - if err := t.fs.WriteFile(path, data); err != nil { - return err - } - - return nil -} diff --git a/pkg/resmgr/changeset.go b/pkg/resmgr/changeset.go new file mode 100644 index 0000000..c6b2e4f --- /dev/null +++ b/pkg/resmgr/changeset.go @@ -0,0 +1,55 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import "fmt" + +// Action resents the action type performed by the reconciliation process. +type Action string + +const ( + CreatedAction Action = "created" + ConfiguredAction Action = "configured" + UnchangedAction Action = "unchanged" + DeletedAction Action = "deleted" +) + +// ChangeSet holds the result of the reconciliation of an object collection. +type ChangeSet struct { + Entries []ChangeSetEntry +} + +func NewChangeSet() *ChangeSet { + return &ChangeSet{Entries: []ChangeSetEntry{}} +} + +func (c *ChangeSet) Add(e ChangeSetEntry) { + c.Entries = append(c.Entries, e) +} + +// ChangeSetEntry defines the result of an action performed on an object. +type ChangeSetEntry struct { + // Subject represents the Object ID in the format 'kind/namespace/name'. + Subject string + // Action represents the action type taken by the reconciler for this object. + Action string +} + +func (e ChangeSetEntry) String() string { + return fmt.Sprintf("%s %s", e.Subject, e.Action) +} diff --git a/pkg/resmgr/client.go b/pkg/resmgr/client.go new file mode 100644 index 0000000..cd3eb1f --- /dev/null +++ b/pkg/resmgr/client.go @@ -0,0 +1,108 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "fmt" + "runtime" + "strings" + + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +func newScheme() *apiruntime.Scheme { + scheme := apiruntime.NewScheme() + _ = apiextensionsv1.AddToScheme(scheme) + _ = corev1.AddToScheme(scheme) + return scheme +} + +func newKubeClient(kubeConfigPath string, kubeContext string) (client.WithWatch, error) { + cfg, err := newKubeConfig(kubeConfigPath, kubeContext) + if err != nil { + return nil, fmt.Errorf("kubernetes client initialization failed: %w", err) + } + + kubeClient, err := client.NewWithWatch(cfg, client.Options{ + Scheme: newScheme(), + }) + if err != nil { + return nil, fmt.Errorf("kubernetes client initialization failed: %w", err) + } + + return kubeClient, nil +} + +func newKubeStatusPoller(kubeConfigPath string, kubeContext string) (*polling.StatusPoller, error) { + kubeConfig, err := newKubeConfig(kubeConfigPath, kubeContext) + if err != nil { + return nil, err + } + + restMapper, err := apiutil.NewDynamicRESTMapper(kubeConfig) + if err != nil { + return nil, err + } + c, err := client.New(kubeConfig, client.Options{Mapper: restMapper}) + if err != nil { + return nil, err + } + + return polling.NewStatusPoller(c, restMapper), nil +} + +func newKubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error) { + configFiles := splitKubeConfigPath(kubeConfigPath) + configOverrides := clientcmd.ConfigOverrides{} + + if len(kubeContext) > 0 { + configOverrides.CurrentContext = kubeContext + } + + cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{Precedence: configFiles}, + &configOverrides, + ).ClientConfig() + + if err != nil { + return nil, fmt.Errorf("kubeconfig load failed: %w", err) + } + + cfg.QPS = 50 + cfg.Burst = 100 + + return cfg, nil +} + +func splitKubeConfigPath(path string) []string { + var sep string + switch runtime.GOOS { + case "windows": + sep = ";" + default: + sep = ":" + } + return strings.Split(path, sep) +} diff --git a/pkg/resmgr/doc.go b/pkg/resmgr/doc.go new file mode 100644 index 0000000..2cdec5b --- /dev/null +++ b/pkg/resmgr/doc.go @@ -0,0 +1,26 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr contains utilities for managing Kubernetes resources. +// +// The Resource Manager performs the following actions: +// - decodes raw manifests (YAML & JSON) into Kubernetes objects +// - validates the objects with server-side dry-run apply +// - determines if the in-cluster objects are in drift based on the dry-run result +// - reconciles the objects on the cluster with server-side apply +// - waits for the objects to be fully reconciled by looking up their readiness status +package resmgr diff --git a/pkg/resmgr/fmt.go b/pkg/resmgr/fmt.go new file mode 100644 index 0000000..8560685 --- /dev/null +++ b/pkg/resmgr/fmt.go @@ -0,0 +1,53 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/object" +) + +const fmtSeparator = "/" + +type ResourceFormatter struct { + Separator string +} + +func (rf *ResourceFormatter) ObjMetadata(obj object.ObjMetadata) string { + var builder strings.Builder + builder.WriteString(obj.GroupKind.Kind + rf.getSeparator()) + if obj.Namespace != "" { + builder.WriteString(obj.Namespace + rf.getSeparator()) + } + builder.WriteString(obj.Name) + return builder.String() +} + +func (rf *ResourceFormatter) Unstructured(obj *unstructured.Unstructured) string { + return rf.ObjMetadata(object.UnstructuredToObjMeta(obj)) +} + +func (rf *ResourceFormatter) getSeparator() string { + if rf.Separator == "" { + return fmtSeparator + } + + return rf.Separator +} diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go new file mode 100644 index 0000000..8d3aece --- /dev/null +++ b/pkg/resmgr/manager.go @@ -0,0 +1,397 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "context" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + "time" + + apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +// ResourceManager reconciles Kubernetes resources onto the target cluster. +type ResourceManager struct { + kubeClient client.WithWatch + kstatusPoller *polling.StatusPoller + fmt *ResourceFormatter + fieldOwner string +} + +// NewResourceManager creates a ResourceManager for the given Kubernetes client config and context. +func NewResourceManager(kubeConfigPath, kubeContext, fieldOwner string) (*ResourceManager, error) { + kubeClient, err := newKubeClient(kubeConfigPath, kubeContext) + if err != nil { + return nil, fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(kubeConfigPath, kubeContext) + if err != nil { + return nil, fmt.Errorf("status poller init failed: %w", err) + } + + return &ResourceManager{ + kubeClient: kubeClient, + kstatusPoller: statusPoller, + fmt: &ResourceFormatter{}, + fieldOwner: fieldOwner, + }, nil +} + +// Read decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. +func (kc *ResourceManager) Read(r io.Reader) (*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + return nil, err + } + + return obj, nil +} + +// ReadAll decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects. +func (kc *ResourceManager) ReadAll(r io.Reader) ([]*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + objects := make([]*unstructured.Unstructured, 0) + + for { + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + if err == io.EOF { + err = nil + break + } + return objects, err + } + + if obj.IsList() { + err = obj.EachListItem(func(item apiruntime.Object) error { + obj := item.(*unstructured.Unstructured) + objects = append(objects, obj) + return nil + }) + if err != nil { + return objects, err + } + continue + } + + objects = append(objects, obj) + } + + sort.Sort(ApplyOrder(objects)) + return objects, nil +} + +// Reconcile performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. +// Drift detection is performed by comparing the server-side dry-run result with the existing object. +// When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. +func (kc *ResourceManager) Reconcile(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) + } + return kc.Reconcile(ctx, object, force) + } + } + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + // do not apply objects that have not drifted to avoid bumping the resource version + if !kc.hasDrifted(existingObject, dryRunObject) { + return kc.changeSetEntry(object, UnchangedAction), nil + } + + appliedObject := object.DeepCopy() + if err := kc.apply(ctx, appliedObject); err != nil { + return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + } + + if dryRunObject.GetResourceVersion() == "" { + return kc.changeSetEntry(appliedObject, CreatedAction), nil + } + return kc.changeSetEntry(appliedObject, ConfiguredAction), nil +} + +// ReconcileAll performs a server-side apply of the given set of objects. +func (kc *ResourceManager) ReconcileAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { + sort.Sort(ApplyOrder(objects)) + changeSet := NewChangeSet() + var toApply []*unstructured.Unstructured + for _, object := range objects { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) + } + return kc.ReconcileAll(ctx, objects, force) + } + } + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + if kc.hasDrifted(existingObject, dryRunObject) { + toApply = append(toApply, object) + if dryRunObject.GetResourceVersion() == "" { + changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) + } else { + changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) + } + } else { + changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) + } + } + + for _, object := range toApply { + appliedObject := object.DeepCopy() + if err := kc.apply(ctx, appliedObject); err != nil { + return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + } + } + + return changeSet, nil +} + +// DeleteAll deletes the given set of objects. +func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { + sort.Sort(sort.Reverse(ApplyOrder(objects))) + changeSet := NewChangeSet() + + for _, object := range objects { + existingObject := object.DeepCopy() + err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("%s query failed, error: %w", kc.fmt.Unstructured(object), err) + } + } else { + err := kc.kubeClient.Delete(ctx, existingObject) + if err != nil { + return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) + } else { + changeSet.Add(*kc.changeSetEntry(object, DeletedAction)) + } + } + } + return changeSet, nil +} + +func (kc *ResourceManager) dryRunApply(ctx context.Context, object *unstructured.Unstructured) error { + opts := []client.PatchOption{ + client.DryRunAll, + client.ForceOwnership, + client.FieldOwner(kc.fieldOwner), + } + return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) +} + +func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { + opts := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner(kc.fieldOwner), + } + return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) +} + +// hasDrifted detects changes to metadata labels, metadata annotations and spec. +func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { + if dryRunObject.GetResourceVersion() == "" || existingObject == nil { + return true + } + + if !apiequality.Semantic.DeepDerivative(dryRunObject.GetLabels(), existingObject.GetLabels()) { + return true + } + + if !apiequality.Semantic.DeepDerivative(dryRunObject.GetAnnotations(), existingObject.GetAnnotations()) { + return true + } + + if _, ok := existingObject.Object["spec"]; ok { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { + return true + } + } else { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { + return true + } + } + + return false +} + +func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { + return &ChangeSetEntry{kc.fmt.Unstructured(object), string(action)} +} + +// Wait checks if the given set of objects has been fully reconciled. +func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { + objectsMeta := object.UnstructuredsToObjMetas(objects) + statusCollector := collector.NewResourceStatusCollector(objectsMeta) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + opts := polling.Options{ + PollInterval: interval, + UseCache: true, + } + eventsChan := kc.kstatusPoller.Poll(ctx, objectsMeta, opts) + + lastStatus := make(map[object.ObjMetadata]*event.ResourceStatus) + + done := statusCollector.ListenWithObserver(eventsChan, collector.ObserverFunc( + func(statusCollector *collector.ResourceStatusCollector, e event.Event) { + var rss []*event.ResourceStatus + for _, rs := range statusCollector.ResourceStatuses { + if rs == nil { + continue + } + if rs.Error == nil { + lastStatus[rs.Identifier] = rs + } + rss = append(rss, rs) + } + desired := status.CurrentStatus + aggStatus := aggregator.AggregateStatus(rss, desired) + if aggStatus == desired { + cancel() + return + } + }), + ) + + <-done + + if statusCollector.Error != nil { + return statusCollector.Error + } + + if ctx.Err() == context.DeadlineExceeded { + var errors = []string{} + for id, rs := range statusCollector.ResourceStatuses { + if rs == nil { + errors = append(errors, fmt.Sprintf("can't determine status for %s", kc.fmt.ObjMetadata(id))) + continue + } + if lastStatus[id].Status != status.CurrentStatus { + var builder strings.Builder + builder.WriteString(fmt.Sprintf("%s status: '%s'", + kc.fmt.ObjMetadata(rs.Identifier), lastStatus[id].Status)) + if rs.Error != nil { + builder.WriteString(fmt.Sprintf(": %s", rs.Error)) + } + errors = append(errors, builder.String()) + } + } + return fmt.Errorf("timeout waiting for: [%s]", strings.Join(errors, ", ")) + } + + return nil +} + +// WaitForTermination waits for the given objects to be deleted from the cluster. +func (kc *ResourceManager) WaitForTermination(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, object := range objects { + if err := wait.PollImmediate(interval, timeout, kc.isDeleted(ctx, object)); err != nil { + return err + } + } + return nil +} + +func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { + return func() (bool, error) { + obj := object.DeepCopy() + err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } +} + +// ToYAML encodes the given Kubernetes API objects to a YAML multi-doc. +func (kc *ResourceManager) ToYAML(objects []*unstructured.Unstructured) (string, error) { + var builder strings.Builder + for _, obj := range objects { + data, err := yaml.Marshal(obj) + if err != nil { + return "", err + } + builder.Write(data) + builder.WriteString("---\n") + } + return builder.String(), nil +} + +// ToJSON encodes the given Kubernetes API objects to a YAML multi-doc. +func (kc *ResourceManager) ToJSON(objects []*unstructured.Unstructured) (string, error) { + list := struct { + ApiVersion string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + Items []*unstructured.Unstructured `json:"items,omitempty"` + }{ + ApiVersion: "v1", + Kind: "List", + Items: objects, + } + + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/pkg/resmgr/sort.go b/pkg/resmgr/sort.go new file mode 100644 index 0000000..529b480 --- /dev/null +++ b/pkg/resmgr/sort.go @@ -0,0 +1,72 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// ApplyOrder implements the Sort interface for unstructured objects. +type ApplyOrder []*unstructured.Unstructured + +func (objects ApplyOrder) Len() int { + return len(objects) +} + +func (objects ApplyOrder) Swap(i, j int) { + objects[i], objects[j] = objects[j], objects[i] +} + +func (objects ApplyOrder) Less(i, j int) bool { + ki := objects[i].GetKind() + ni := objects[i].GetName() + kj := objects[j].GetKind() + nj := objects[j].GetName() + ranki, rankj := rankOfKind(ki), rankOfKind(kj) + if ranki == rankj { + return ni < nj + } + return ranki < rankj +} + +// rankOfKind returns an int denoting the position of the given kind +// in the partial ordering of Kubernetes resources, according to which +// kinds depend on which (derived by hand). +func rankOfKind(kind string) int { + switch strings.ToLower(kind) { + // API extensions + case "customresourcedefinition": + return 0 + // Global objects + case "namespace", "clusterrolebinding", "clusterrole": + return 1 + // Namespaced objects + case "serviceaccount", "role", "rolebinding", "service", "endpoint", "ingress": + return 2 + // Namespaced objects + case "resourcequota", "limitrange", "secret", "configmap", "persistentvolume", "persistentvolumeclaim": + return 2 + // Workload objects + case "daemonset", "deployment", "job", "cronjob", "statefulset", "replicationcontroller", "replicaset", "pod": + return 3 + default: + return 4 + } +} From 5ce258fab2c0dc31de5ab78b22a16b2400fbba10 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 00:24:49 +0300 Subject: [PATCH 02/40] Refactor CI Signed-off-by: Stefan Prodan --- .github/actions/kustomize/Dockerfile | 6 ---- .github/actions/kustomize/action.yml | 9 ----- .github/actions/kustomize/entrypoint.sh | 12 ------- .github/workflows/action-test.yaml | 18 ---------- .github/workflows/e2e.yaml | 23 ++++++------ .goreleaser.yml | 1 - action/Dockerfile | 6 ---- action/action.yml | 48 +++++++++++++++++++++---- action/entrypoint.sh | 17 --------- testdata/plain/common/rbac.yaml | 6 ++-- 10 files changed, 55 insertions(+), 91 deletions(-) delete mode 100644 .github/actions/kustomize/Dockerfile delete mode 100644 .github/actions/kustomize/action.yml delete mode 100644 .github/actions/kustomize/entrypoint.sh delete mode 100644 .github/workflows/action-test.yaml delete mode 100644 action/Dockerfile delete mode 100644 action/entrypoint.sh diff --git a/.github/actions/kustomize/Dockerfile b/.github/actions/kustomize/Dockerfile deleted file mode 100644 index 2ebd633..0000000 --- a/.github/actions/kustomize/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM giantswarm/tiny-tools - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/kustomize/action.yml b/.github/actions/kustomize/action.yml deleted file mode 100644 index bd53d1d..0000000 --- a/.github/actions/kustomize/action.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: 'kustomize' -description: 'A GitHub Action to run kustomize commands' -author: 'Stefan Prodan' -branding: - icon: 'command' - color: 'blue' -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/kustomize/entrypoint.sh b/.github/actions/kustomize/entrypoint.sh deleted file mode 100644 index db8be7e..0000000 --- a/.github/actions/kustomize/entrypoint.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -l - -VERSION=3.5.4 -curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${VERSION}/kustomize_v${VERSION}_linux_amd64.tar.gz | tar xz - -mkdir -p $GITHUB_WORKSPACE/bin -cp ./kustomize $GITHUB_WORKSPACE/bin -chmod +x $GITHUB_WORKSPACE/bin/kustomize -ls -lh $GITHUB_WORKSPACE/bin - -echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH -echo "$RUNNER_WORKSPACE/$(basename $GITHUB_REPOSITORY)/bin" >> $GITHUB_PATH diff --git a/.github/workflows/action-test.yaml b/.github/workflows/action-test.yaml deleted file mode 100644 index 28ec395..0000000 --- a/.github/workflows/action-test.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: action-test - -on: - push: - branches: - - '*' - -jobs: - action: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Install Kustomizer - uses: ./action - - name: Run Kustomizer - run: | - kustomizer -v diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 0c17536..c991c21 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -2,9 +2,9 @@ name: e2e on: pull_request: + branches: [main] push: - branches: - - master + branches: [main] jobs: kind: @@ -22,11 +22,12 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: 1.14.x + go-version: 1.16.x - name: Setup Kubernetes uses: engineerd/setup-kind@v0.5.0 - - name: Setup Kustomize - uses: ./.github/actions/kustomize + with: + version: v0.11.1 + image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 - name: Run test run: make test - name: Check if working tree is dirty @@ -36,15 +37,13 @@ jobs: exit 1 fi - name: Build - run: sudo go build -o ./bin/kustomizer ./cmd/kustomizer + run: make build - name: Smoke tests run: | - ./bin/kustomizer apply testdata/plain/ --name=test --revision=1.0.0 - kubectl -n kustomizer-demo get svc frontend 2>&1 | grep frontend - rm -rf testdata/plain/frontend - ./bin/kustomizer apply testdata/plain/ --name=test --revision=1.1.0 - kubectl -n kustomizer-demo get svc frontend 2>&1 | grep NotFound - ./bin/kustomizer delete --name=test + ./bin/kustomizer apply -f testdata/plain/ --wait + kubectl -n kustomizer-demo get svc frontend 2>&1 | grep frontend + /bin/kustomizer delete -f testdata/plain/ --wait + kubectl get ns kustomizer-demo 2>&1 | grep NotFound - name: Debug failure if: failure() run: | diff --git a/.goreleaser.yml b/.goreleaser.yml index 1dc42c5..e98630c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,7 +20,6 @@ brews: - tap: owner: stefanprodan name: kustomizer - folder: Formula homepage: "https://kustomizer.dev/" description: "Kustomize build, apply, prune command-line utility." diff --git a/action/Dockerfile b/action/Dockerfile deleted file mode 100644 index 97429a0..0000000 --- a/action/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM stefanprodan/alpine-base:latest - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/action/action.yml b/action/action.yml index 982b9fc..31ce05f 100644 --- a/action/action.yml +++ b/action/action.yml @@ -1,9 +1,43 @@ -name: 'kustomizer' -description: 'A GitHub Action to run kustomizer commands' -author: 'Stefan Prodan' +name: Setup Kustomizer CLI +description: A GitHub Action for running kustomizer commands +author: Stefan Prodan branding: - icon: 'command' - color: 'blue' + color: blue + icon: command +inputs: + version: + description: "Kustomizer version e.g. 0.8.0 (defaults to latest stable release)" + required: false + arch: + description: "arch can be amd64, arm64 or arm" + required: true + default: "amd64" runs: - using: 'docker' - image: 'Dockerfile' + using: composite + steps: + - name: "Download Kustomizer binary to tmp" + shell: bash + run: | + ARCH=${{ inputs.arch }} + VERSION=${{ inputs.version }} + + if [ -z $VERSION ]; then + VERSION=$(curl https://api.github.com/repos/stefanprodan/kustomizer/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) + fi + + BIN_URL="https://github.com/stefanprodan/kustomizer/releases/download/v${VERSION}/flux_${VERSION}_linux_${ARCH}.tar.gz" + curl -sL ${BIN_URL} -o /tmp/kustomizer.tar.gz + mkdir -p /tmp/kustomizer + tar -C /tmp/kustomizer/ -zxvf /tmp/kustomizer.tar.gz + - name: "Add kustomizer binary to /usr/local/bin" + shell: bash + run: | + sudo cp /tmp/kustomizer/kustomizer /usr/local/bin + - name: "Cleanup tmp" + shell: bash + run: | + rm -rf /tmp/kustomizer/ /tmp/kustomizer.tar.gz + - name: "Verify correct installation of binary" + shell: bash + run: | + kustomizer -v diff --git a/action/entrypoint.sh b/action/entrypoint.sh deleted file mode 100644 index 3cb8f4c..0000000 --- a/action/entrypoint.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -e - -curl -s https://api.github.com/repos/stefanprodan/kustomizer/releases/latest |\ - grep browser_download |\ - grep linux |\ - cut -d '"' -f 4 |\ - xargs curl -sL -o kustomizer.tar.gz - -tar xzf ./kustomizer.tar.gz -rm ./kustomizer.tar.gz - -mkdir -p $GITHUB_WORKSPACE/bin -mv ./kustomizer $GITHUB_WORKSPACE/bin - -echo "::add-path::$GITHUB_WORKSPACE/bin" -echo "::add-path::$RUNNER_WORKSPACE/$(basename $GITHUB_REPOSITORY)/bin" diff --git a/testdata/plain/common/rbac.yaml b/testdata/plain/common/rbac.yaml index 1df6ff9..9c64104 100644 --- a/testdata/plain/common/rbac.yaml +++ b/testdata/plain/common/rbac.yaml @@ -1,7 +1,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: demo-read-only + name: kustomizer-demo-read-only rules: - apiGroups: - apps @@ -14,11 +14,11 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: demo-read-only + name: kustomizer-demo-read-only roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: demo-read-only + name: kustomizer-demo-read-only subjects: - kind: ServiceAccount name: demo From 73cd092c20d8efb2425be67b8f632b803017dc3b Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 00:38:02 +0300 Subject: [PATCH 03/40] Add filename validation Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 2 +- cmd/kustomizer/apply.go | 4 ++++ cmd/kustomizer/delete.go | 3 +++ cmd/kustomizer/main.go | 2 +- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c991c21..44eae47 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -42,7 +42,7 @@ jobs: run: | ./bin/kustomizer apply -f testdata/plain/ --wait kubectl -n kustomizer-demo get svc frontend 2>&1 | grep frontend - /bin/kustomizer delete -f testdata/plain/ --wait + ./bin/kustomizer delete -f testdata/plain/ --wait kubectl get ns kustomizer-demo 2>&1 | grep NotFound - name: Debug failure if: failure() diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 03188b3..4686003 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -77,6 +77,10 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { } objects = append(objects, objs...) } else { + if len(applyArgs.filename) == 0 { + return fmt.Errorf("-f or -k is required") + } + manifests, err := scan(applyArgs.filename) if err != nil { return err diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 0bbf350..b86d79a 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -73,6 +73,9 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { } objects = append(objects, objs...) } else { + if len(deleteArgs.filename) == 0 { + return fmt.Errorf("-f or -k is required") + } manifests, err := scan(deleteArgs.filename) if err != nil { return err diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index 2569096..f134fbf 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -28,7 +28,7 @@ import ( "k8s.io/klog/v2" ) -var VERSION = "0.0.0-dev.0" +var VERSION = "1.0.0-dev.0" var rootCmd = &cobra.Command{ Use: "kustomizer", From 3713811b83930004bf295c6ac8aac9e4ba4696e3 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 12:26:42 +0300 Subject: [PATCH 04/40] Refactor ResourceManager Signed-off-by: Stefan Prodan --- pkg/resmgr/doc.go | 6 ++- pkg/resmgr/manager.go | 100 +++++------------------------------------- 2 files changed, 14 insertions(+), 92 deletions(-) diff --git a/pkg/resmgr/doc.go b/pkg/resmgr/doc.go index 2cdec5b..44ab388 100644 --- a/pkg/resmgr/doc.go +++ b/pkg/resmgr/doc.go @@ -17,10 +17,12 @@ limitations under the License. // Package resmgr contains utilities for managing Kubernetes resources. // -// The Resource Manager performs the following actions: -// - decodes raw manifests (YAML & JSON) into Kubernetes objects +// The ResourceManager performs the following actions: +// - orders the Kubernetes objects for apply (CRDs, Namespaces, ClusterRoles first) // - validates the objects with server-side dry-run apply // - determines if the in-cluster objects are in drift based on the dry-run result // - reconciles the objects on the cluster with server-side apply // - waits for the objects to be fully reconciled by looking up their readiness status +// - deletes objects that are subject to garbage collection +// - waits for the deleted objects to be terminated package resmgr diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index 8d3aece..a51d7b9 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -19,9 +19,7 @@ package resmgr import ( "context" - "encoding/json" "fmt" - "io" "sort" "strings" "time" @@ -30,9 +28,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" - yamlutil "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator" "sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector" @@ -40,7 +36,6 @@ import ( "sigs.k8s.io/cli-utils/pkg/kstatus/status" "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" ) // ResourceManager reconciles Kubernetes resources onto the target cluster. @@ -71,57 +66,10 @@ func NewResourceManager(kubeConfigPath, kubeContext, fieldOwner string) (*Resour }, nil } -// Read decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. -func (kc *ResourceManager) Read(r io.Reader) (*unstructured.Unstructured, error) { - reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) - obj := &unstructured.Unstructured{} - err := reader.Decode(obj) - if err != nil { - return nil, err - } - - return obj, nil -} - -// ReadAll decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects. -func (kc *ResourceManager) ReadAll(r io.Reader) ([]*unstructured.Unstructured, error) { - reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) - objects := make([]*unstructured.Unstructured, 0) - - for { - obj := &unstructured.Unstructured{} - err := reader.Decode(obj) - if err != nil { - if err == io.EOF { - err = nil - break - } - return objects, err - } - - if obj.IsList() { - err = obj.EachListItem(func(item apiruntime.Object) error { - obj := item.(*unstructured.Unstructured) - objects = append(objects, obj) - return nil - }) - if err != nil { - return objects, err - } - continue - } - - objects = append(objects, obj) - } - - sort.Sort(ApplyOrder(objects)) - return objects, nil -} - -// Reconcile performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. +// Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. // Drift detection is performed by comparing the server-side dry-run result with the existing object. // When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. -func (kc *ResourceManager) Reconcile(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { +func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) @@ -133,7 +81,7 @@ func (kc *ResourceManager) Reconcile(ctx context.Context, object *unstructured.U return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", kc.fmt.Unstructured(dryRunObject), err) } - return kc.Reconcile(ctx, object, force) + return kc.Apply(ctx, object, force) } } return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) @@ -155,8 +103,9 @@ func (kc *ResourceManager) Reconcile(ctx context.Context, object *unstructured.U return kc.changeSetEntry(appliedObject, ConfiguredAction), nil } -// ReconcileAll performs a server-side apply of the given set of objects. -func (kc *ResourceManager) ReconcileAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { +// ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, +// it applies the objects that are new or modified. +func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { sort.Sort(ApplyOrder(objects)) changeSet := NewChangeSet() var toApply []*unstructured.Unstructured @@ -172,7 +121,7 @@ func (kc *ResourceManager) ReconcileAll(ctx context.Context, objects []*unstruct return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", kc.fmt.Unstructured(dryRunObject), err) } - return kc.ReconcileAll(ctx, objects, force) + return kc.ApplyAll(ctx, objects, force) } } return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) @@ -362,36 +311,7 @@ func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.U } } -// ToYAML encodes the given Kubernetes API objects to a YAML multi-doc. -func (kc *ResourceManager) ToYAML(objects []*unstructured.Unstructured) (string, error) { - var builder strings.Builder - for _, obj := range objects { - data, err := yaml.Marshal(obj) - if err != nil { - return "", err - } - builder.Write(data) - builder.WriteString("---\n") - } - return builder.String(), nil -} - -// ToJSON encodes the given Kubernetes API objects to a YAML multi-doc. -func (kc *ResourceManager) ToJSON(objects []*unstructured.Unstructured) (string, error) { - list := struct { - ApiVersion string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - Items []*unstructured.Unstructured `json:"items,omitempty"` - }{ - ApiVersion: "v1", - Kind: "List", - Items: objects, - } - - data, err := json.MarshalIndent(list, "", " ") - if err != nil { - return "", err - } - - return string(data), nil +// KubeClient returns the underlying controller-runtime client. +func (kc *ResourceManager) KubeClient() client.Client { + return kc.kubeClient } From 4d6a31343a0d2f405a41f840a9947371b5acbf53 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 12:28:52 +0300 Subject: [PATCH 05/40] Add inventory package The InventoryManager contains utilities for keeping a record of Kubernetes objects applied on a cluster. Signed-off-by: Stefan Prodan --- pkg/inventory/doc.go | 24 ++++ pkg/inventory/inventory.go | 121 ++++++++++++++++++++ pkg/inventory/manager.go | 222 +++++++++++++++++++++++++++++++++++++ pkg/inventory/sort.go | 65 +++++++++++ 4 files changed, 432 insertions(+) create mode 100644 pkg/inventory/doc.go create mode 100644 pkg/inventory/inventory.go create mode 100644 pkg/inventory/manager.go create mode 100644 pkg/inventory/sort.go diff --git a/pkg/inventory/doc.go b/pkg/inventory/doc.go new file mode 100644 index 0000000..42e4e9a --- /dev/null +++ b/pkg/inventory/doc.go @@ -0,0 +1,24 @@ +/* +Copyright 2021 Stefan Prodan +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 inventory contains utilities for keeping a record of Kubernetes objects applied on a cluster. +// +// The InventoryManager performs the following actions: +// - decodes raw manifests (YAML & JSON) into Kubernetes objects +// - records the objects metadata and stores the inventory in a Kubernetes ConfigMap +// - determines which objects are subject to garbage collection +package inventory diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go new file mode 100644 index 0000000..f75b59b --- /dev/null +++ b/pkg/inventory/inventory.go @@ -0,0 +1,121 @@ +/* +Copyright 2021 Stefan Prodan +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 inventory + +import ( + "sort" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/object" +) + +// Inventory is a record of objects that are applied on a cluster. +type Inventory struct { + Entries map[string]string `json:"entries"` +} + +func NewInventory() *Inventory { + return &Inventory{Entries: map[string]string{}} +} + +// Add adds the given objects to the inventory. +func (inv *Inventory) Add(objects []*unstructured.Unstructured) error { + for _, om := range objects { + objMeta := object.UnstructuredToObjMeta(om) + gv, err := schema.ParseGroupVersion(om.GetAPIVersion()) + if err != nil { + return err + } + inv.Entries[objMeta.String()] = gv.Version + } + + return nil +} + +// List returns the inventory entries as unstructured.Unstructured objects. +func (inv *Inventory) List() ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + list, err := inv.ListMeta() + if err != nil { + return nil, err + } + + for _, metadata := range list { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.GroupKind.Group, + Kind: metadata.GroupKind.Kind, + Version: inv.Entries[metadata.String()], + }) + u.SetName(metadata.Name) + u.SetNamespace(metadata.Namespace) + objects = append(objects, u) + } + + sort.Sort(InventoryOrder(objects)) + return objects, nil +} + +// ListMeta returns the inventory entries as object.ObjMetadata objects. +func (inv *Inventory) ListMeta() ([]object.ObjMetadata, error) { + var metas []object.ObjMetadata + for e, _ := range inv.Entries { + m, err := object.ParseObjMetadata(e) + if err != nil { + return metas, err + } + metas = append(metas, m) + } + + return metas, nil +} + +// Diff returns the slice of objects that do not exist in the target inventory. +func (inv *Inventory) Diff(target *Inventory) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + aList, err := inv.ListMeta() + if err != nil { + return nil, err + } + + bList, err := target.ListMeta() + if err != nil { + return nil, err + } + + list := object.SetDiff(aList, 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: inv.Entries[metadata.String()], + }) + u.SetName(metadata.Name) + u.SetNamespace(metadata.Namespace) + objects = append(objects, u) + } + + sort.Sort(InventoryOrder(objects)) + return objects, nil +} diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go new file mode 100644 index 0000000..f212fa6 --- /dev/null +++ b/pkg/inventory/manager.go @@ -0,0 +1,222 @@ +/* +Copyright 2021 Stefan Prodan +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 inventory + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apiruntime "k8s.io/apimachinery/pkg/runtime" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +// InventoryManager records the Kubernetes objects that are applied on the cluster. +type InventoryManager struct { + fieldOwner string +} + +// NewInventoryManager returns an InventoryManager. +func NewInventoryManager(fieldOwner string) *InventoryManager { + return &InventoryManager{fieldOwner: fieldOwner} +} + +// GetStaleObjects returns the list of objects subject to pruning. +func (im *InventoryManager) GetStaleObjects(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + exInv, err := im.Retrieve(ctx, kubeClient, name, namespace) + if err != nil { + if apierrors.IsNotFound(err) { + return objects, nil + } + return nil, err + } + + objects, err = exInv.Diff(inv) + if err != nil { + return nil, err + } + + return objects, nil +} + +// Store applies the Inventory object on the server. +func (im *InventoryManager) Store(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) error { + data, err := json.Marshal(inv.Entries) + if err != nil { + return err + } + + cm := im.newConfigMap(name, namespace) + cm.Data = map[string]string{ + "inventory": string(data), + } + + opts := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner(im.fieldOwner), + } + return kubeClient.Patch(ctx, cm, client.Apply, opts...) +} + +// Retrieve fetches the Inventory object from the server. +func (im *InventoryManager) Retrieve(ctx context.Context, kubeClient client.Client, name, namespace string) (*Inventory, error) { + cm := im.newConfigMap(name, namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := kubeClient.Get(ctx, cmKey, cm) + if err != nil { + return nil, err + } + + if _, ok := cm.Data["inventory"]; !ok { + return nil, fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) + } + + var entries map[string]string + err = json.Unmarshal([]byte(cm.Data["inventory"]), &entries) + if err != nil { + return nil, err + } + + return &Inventory{Entries: entries}, nil +} + +// Remove deletes the Inventory object from the server. +func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client, name, namespace string) error { + cm := im.newConfigMap(name, namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := kubeClient.Delete(ctx, cm) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete ConfigMap/%s, error: %w", cmKey, err) + } + return nil +} + +// Record creates an Inventory of the given objects. +func (im *InventoryManager) Record(objects []*unstructured.Unstructured) (*Inventory, error) { + inventory := NewInventory() + + if err := inventory.Add(objects); err != nil { + return nil, err + } + + return inventory, nil +} + +// Read decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. +func (im *InventoryManager) Read(r io.Reader) (*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + return nil, err + } + + return obj, nil +} + +// ReadAll decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects. +func (im *InventoryManager) ReadAll(r io.Reader) ([]*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + objects := make([]*unstructured.Unstructured, 0) + + for { + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + if err == io.EOF { + err = nil + break + } + return objects, err + } + + if obj.IsList() { + err = obj.EachListItem(func(item apiruntime.Object) error { + obj := item.(*unstructured.Unstructured) + objects = append(objects, obj) + return nil + }) + if err != nil { + return objects, err + } + continue + } + + objects = append(objects, obj) + } + + return objects, nil +} + +// ToYAML encodes the given Kubernetes API objects to a YAML multi-doc. +func (im *InventoryManager) ToYAML(objects []*unstructured.Unstructured) (string, error) { + var builder strings.Builder + for _, obj := range objects { + data, err := yaml.Marshal(obj) + if err != nil { + return "", err + } + builder.Write(data) + builder.WriteString("---\n") + } + return builder.String(), nil +} + +// ToJSON encodes the given Kubernetes API objects to a YAML multi-doc. +func (im *InventoryManager) ToJSON(objects []*unstructured.Unstructured) (string, error) { + list := struct { + ApiVersion string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + Items []*unstructured.Unstructured `json:"items,omitempty"` + }{ + ApiVersion: "v1", + Kind: "ListMeta", + Items: objects, + } + + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + return "", err + } + + return string(data), nil +} + +func (im *InventoryManager) newConfigMap(name, namespace string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/pkg/inventory/sort.go b/pkg/inventory/sort.go new file mode 100644 index 0000000..7659b4f --- /dev/null +++ b/pkg/inventory/sort.go @@ -0,0 +1,65 @@ +/* +Copyright 2021 Stefan Prodan +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 inventory + +import ( + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// InventoryOrder implements the Sort interface for unstructured objects. +type InventoryOrder []*unstructured.Unstructured + +func (objects InventoryOrder) Len() int { + return len(objects) +} + +func (objects InventoryOrder) Swap(i, j int) { + objects[i], objects[j] = objects[j], objects[i] +} + +func (objects InventoryOrder) Less(i, j int) bool { + ki := objects[i].GetKind() + ni := objects[i].GetName() + kj := objects[j].GetKind() + nj := objects[j].GetName() + ranki, rankj := rankOfKind(ki), rankOfKind(kj) + if ranki == rankj { + return ni < nj + } + return ranki < rankj +} + +// rankOfKind returns an int denoting the position of the given kind +// in the partial ordering of Kubernetes resources. +func rankOfKind(kind string) int { + switch strings.ToLower(kind) { + // API extensions + case "customresourcedefinition": + return 0 + // Namespace objects + case "namespace": + return 1 + // Global objects + case "clusterrole", "clusterrolebinding", "ingressclass", "storageclass": + return 2 + default: + return 3 + } +} From bcc76fd58086ef3b684c78d765138ae3e4e81ba8 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 12:29:56 +0300 Subject: [PATCH 06/40] Implement garbage collection Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 84 ++++++++++++++++++++++++++++++++-------- cmd/kustomizer/delete.go | 9 +++-- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 4686003..7c3a0d2 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -27,8 +27,10 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/resmgr" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/resmgr" ) var applyCmd = &cobra.Command{ @@ -38,11 +40,14 @@ var applyCmd = &cobra.Command{ } type applyFlags struct { - filename []string - wait bool - force bool - kustomize string - output string + filename []string + kustomize string + output string + inventoryName string + inventoryNamespace string + wait bool + force bool + prune bool } var applyArgs applyFlags @@ -52,17 +57,16 @@ func init() { applyCmd.Flags().StringVarP(&applyArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") applyCmd.Flags().BoolVar(&applyArgs.wait, "wait", false, "wait for the applied Kubernetes objects to become ready") applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "recreate objects that contain immutable fields changes") + applyCmd.Flags().BoolVar(&applyArgs.prune, "prune", false, "delete stale objects") applyCmd.Flags().StringVarP(&applyArgs.output, "output", "o", "", "output can be yaml or json") + applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "inventory name") + applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "inventory namespace") rootCmd.AddCommand(applyCmd) } func runApplyCmd(cmd *cobra.Command, args []string) error { - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "flagger-cli") - if err != nil { - return err - } - + invMgr := inventory.NewInventoryManager("kustomizer") objects := make([]*unstructured.Unstructured, 0) if applyArgs.kustomize != "" { @@ -71,7 +75,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return err } - objs, err := resMgr.ReadAll(bytes.NewReader(data)) + objs, err := invMgr.ReadAll(bytes.NewReader(data)) if err != nil { return fmt.Errorf("%s: %w", applyArgs.kustomize, err) } @@ -91,7 +95,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return err } - objs, err := resMgr.ReadAll(bufio.NewReader(ms)) + objs, err := invMgr.ReadAll(bufio.NewReader(ms)) ms.Close() if err != nil { return fmt.Errorf("%s: %w", manifest, err) @@ -99,19 +103,19 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { objects = append(objects, objs...) } } - sort.Sort(resmgr.ApplyOrder(objects)) + sort.Sort(resmgr.ApplyOrder(objects)) if applyArgs.output != "" { switch applyArgs.output { case "yaml": - yml, err := resMgr.ToYAML(objects) + yml, err := invMgr.ToYAML(objects) if err != nil { return err } fmt.Println(yml) return nil case "json": - json, err := resMgr.ToJSON(objects) + json, err := invMgr.ToJSON(objects) if err != nil { return err } @@ -122,23 +126,69 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { } } + if applyArgs.inventoryName == "" { + return fmt.Errorf("--inventory-name is required") + } + if applyArgs.inventoryNamespace == "" { + return fmt.Errorf("--inventory-namespace is required") + } + + newInventory, err := invMgr.Record(objects) + if err != nil { + return fmt.Errorf("creating inventory failed, error: %w", err) + } + + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "kustomizer") + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() for _, object := range objects { - change, err := resMgr.Reconcile(ctx, object, applyArgs.force) + change, err := resMgr.Apply(ctx, object, applyArgs.force) if err != nil { return err } fmt.Println(change.String()) } + staleObjects, err := invMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + if err != nil { + return fmt.Errorf("inventory query failed, error: %w", err) + } + + err = invMgr.Store(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + if err != nil { + return fmt.Errorf("inventory apply failed, error: %w", err) + } + + if applyArgs.prune && len(staleObjects) > 0 { + changeSet, err := resMgr.DeleteAll(ctx, staleObjects) + if err != nil { + return fmt.Errorf("prune failed, error: %w", err) + } + for _, change := range changeSet.Entries { + fmt.Println(change.String()) + } + } + if applyArgs.wait { fmt.Println("waiting for resources to become ready...") + err = resMgr.Wait(objects, 2*time.Second, rootArgs.timeout) if err != nil { return err } + + if applyArgs.prune && len(staleObjects) > 0 { + err = resMgr.WaitForTermination(staleObjects, 2*time.Second, rootArgs.timeout) + if err != nil { + return fmt.Errorf("wating for termination failed, error: %w", err) + } + } + fmt.Println("all resources are ready") } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index b86d79a..1517a57 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -27,8 +27,10 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/resmgr" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/resmgr" ) var deleteCmd = &cobra.Command{ @@ -54,6 +56,7 @@ func init() { } func deleteCmdRun(cmd *cobra.Command, args []string) error { + invMgr := inventory.NewInventoryManager("kustomizer") resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "flagger-cli") if err != nil { return err @@ -67,7 +70,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return err } - objs, err := resMgr.ReadAll(bytes.NewReader(data)) + objs, err := invMgr.ReadAll(bytes.NewReader(data)) if err != nil { return fmt.Errorf("%s: %w", deleteArgs.kustomize, err) } @@ -86,7 +89,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return err } - objs, err := resMgr.ReadAll(bufio.NewReader(ms)) + objs, err := invMgr.ReadAll(bufio.NewReader(ms)) ms.Close() if err != nil { return fmt.Errorf("%s: %w", manifest, err) From 107f1ab2fe2f164f9318dcf05f4ec88b319ddc2c Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 12:40:23 +0300 Subject: [PATCH 07/40] Add garbage collection e2e tests Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 44eae47..b75405e 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -38,10 +38,17 @@ jobs: fi - name: Build run: make build - - name: Smoke tests + - name: Apply test run: | - ./bin/kustomizer apply -f testdata/plain/ --wait + ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune kubectl -n kustomizer-demo get svc frontend 2>&1 | grep frontend + - name: Prune test + run: | + rm -rf testdata/plain/frontend + ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune + kubectl -n kustomizer-demo get svc frontend 2>&1 | grep NotFound + - name: Delete test + run: | ./bin/kustomizer delete -f testdata/plain/ --wait kubectl get ns kustomizer-demo 2>&1 | grep NotFound - name: Debug failure From 856674033a00dde7fe725e49998bec7b00fb08d8 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 13:20:15 +0300 Subject: [PATCH 08/40] Implement deletion based on inventory Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 2 +- cmd/kustomizer/apply.go | 8 ++-- cmd/kustomizer/delete.go | 79 ++++++++++++++------------------------ cmd/kustomizer/main.go | 4 +- cmd/kustomizer/utils.go | 6 +-- 5 files changed, 38 insertions(+), 61 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index b75405e..8ba446b 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -49,7 +49,7 @@ jobs: kubectl -n kustomizer-demo get svc frontend 2>&1 | grep NotFound - name: Delete test run: | - ./bin/kustomizer delete -f testdata/plain/ --wait + ./bin/kustomizer delete -i kustomizer-demo --wait kubectl get ns kustomizer-demo 2>&1 | grep NotFound - name: Debug failure if: failure() diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 7c3a0d2..e04b0f8 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -59,14 +59,14 @@ func init() { applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "recreate objects that contain immutable fields changes") applyCmd.Flags().BoolVar(&applyArgs.prune, "prune", false, "delete stale objects") applyCmd.Flags().StringVarP(&applyArgs.output, "output", "o", "", "output can be yaml or json") - applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "inventory name") - applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "inventory namespace") + applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "inventory configmap name") + applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "inventory configmap namespace") rootCmd.AddCommand(applyCmd) } func runApplyCmd(cmd *cobra.Command, args []string) error { - invMgr := inventory.NewInventoryManager("kustomizer") + invMgr := inventory.NewInventoryManager(PROJECT) objects := make([]*unstructured.Unstructured, 0) if applyArgs.kustomize != "" { @@ -138,7 +138,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("creating inventory failed, error: %w", err) } - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "kustomizer") + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) if err != nil { return err } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 1517a57..d3af4ce 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -18,17 +18,11 @@ limitations under the License. package main import ( - "bufio" - "bytes" "context" "fmt" - "os" - "sort" "time" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/resmgr" ) @@ -40,68 +34,44 @@ var deleteCmd = &cobra.Command{ } type deleteFlags struct { - filename []string - kustomize string - wait bool + inventoryName string + inventoryNamespace string + wait bool } var deleteArgs deleteFlags func init() { - deleteCmd.Flags().StringSliceVarP(&deleteArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") - deleteCmd.Flags().StringVarP(&deleteArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") - deleteCmd.Flags().BoolVar(&deleteArgs.wait, "wait", false, "wait for the deleted Kubernetes objects to be terminated") + deleteCmd.Flags().StringVarP(&deleteArgs.inventoryName, "inventory-name", "i", "", "inventory configmap name") + deleteCmd.Flags().StringVar(&deleteArgs.inventoryNamespace, "inventory-namespace", "default", "inventory configmap namespace") + deleteCmd.Flags().BoolVar(&deleteArgs.wait, "wait", true, "wait for the deleted Kubernetes objects to be terminated") rootCmd.AddCommand(deleteCmd) } func deleteCmdRun(cmd *cobra.Command, args []string) error { - invMgr := inventory.NewInventoryManager("kustomizer") - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, "flagger-cli") - if err != nil { - return err + if deleteArgs.inventoryName == "" { + return fmt.Errorf("--inventory-name is required") + } + if deleteArgs.inventoryNamespace == "" { + return fmt.Errorf("--inventory-namespace is required") } - objects := make([]*unstructured.Unstructured, 0) - - if applyArgs.kustomize != "" { - data, err := buildKustomization(deleteArgs.kustomize) - if err != nil { - return err - } - - objs, err := invMgr.ReadAll(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("%s: %w", deleteArgs.kustomize, err) - } - objects = append(objects, objs...) - } else { - if len(deleteArgs.filename) == 0 { - return fmt.Errorf("-f or -k is required") - } - manifests, err := scan(deleteArgs.filename) - if err != nil { - return err - } - for _, manifest := range manifests { - ms, err := os.Open(manifest) - if err != nil { - return err - } - - objs, err := invMgr.ReadAll(bufio.NewReader(ms)) - ms.Close() - if err != nil { - return fmt.Errorf("%s: %w", manifest, err) - } - objects = append(objects, objs...) - } + invMgr := inventory.NewInventoryManager(PROJECT) + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) + if err != nil { + return err } - sort.Sort(resmgr.ApplyOrder(objects)) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() + inv, err := invMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + objects, err := inv.List() + if err != nil { + return err + } + changeSet, err := resMgr.DeleteAll(ctx, objects) if err != nil { return err @@ -110,6 +80,13 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { fmt.Println(change.String()) } + err = invMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + if err != nil { + return err + } + + fmt.Println(fmt.Sprintf("ConfigMap/%s/%s deleted", deleteArgs.inventoryNamespace, deleteArgs.inventoryName)) + if deleteArgs.wait { fmt.Println("waiting for resources to be terminated...") err = resMgr.WaitForTermination(objects, 2*time.Second, rootArgs.timeout) diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index f134fbf..4f19d1e 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -30,8 +30,10 @@ import ( var VERSION = "1.0.0-dev.0" +const PROJECT = "kustomizer" + var rootCmd = &cobra.Command{ - Use: "kustomizer", + Use: PROJECT, Version: VERSION, SilenceUsage: true, SilenceErrors: true, diff --git a/cmd/kustomizer/utils.go b/cmd/kustomizer/utils.go index 9cd04e7..235f8de 100644 --- a/cmd/kustomizer/utils.go +++ b/cmd/kustomizer/utils.go @@ -106,10 +106,8 @@ func buildKustomization(base string) ([]byte, error) { } buildOptions := &krusty.Options{ - LoadRestrictions: kustypes.LoadRestrictionsNone, - AddManagedbyLabel: false, - DoPrune: false, - PluginConfig: kustypes.DisabledPluginConfig(), + LoadRestrictions: kustypes.LoadRestrictionsNone, + PluginConfig: kustypes.DisabledPluginConfig(), } k := krusty.MakeKustomizer(buildOptions) From b031784eb3362740842ef2f464a6b7a04dc298f5 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 14:15:54 +0300 Subject: [PATCH 09/40] Add metadata to the inventory configmap Signed-off-by: Stefan Prodan --- pkg/inventory/manager.go | 72 +++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go index f212fa6..f6617bf 100644 --- a/pkg/inventory/manager.go +++ b/pkg/inventory/manager.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "strings" + "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -37,30 +38,33 @@ import ( // InventoryManager records the Kubernetes objects that are applied on the cluster. type InventoryManager struct { fieldOwner string + group string } // NewInventoryManager returns an InventoryManager. -func NewInventoryManager(fieldOwner string) *InventoryManager { - return &InventoryManager{fieldOwner: fieldOwner} +func NewInventoryManager(fieldOwner, group string) (*InventoryManager, error) { + if fieldOwner == "" { + return nil, fmt.Errorf("fieldOwner is required") + } + if group == "" { + return nil, fmt.Errorf("group is required") + } + + return &InventoryManager{ + fieldOwner: fieldOwner, + group: group, + }, nil } -// GetStaleObjects returns the list of objects subject to pruning. -func (im *InventoryManager) GetStaleObjects(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) ([]*unstructured.Unstructured, error) { - objects := make([]*unstructured.Unstructured, 0) - exInv, err := im.Retrieve(ctx, kubeClient, name, namespace) - if err != nil { - if apierrors.IsNotFound(err) { - return objects, nil - } - return nil, err - } +// Record creates an Inventory of the given objects. +func (im *InventoryManager) Record(objects []*unstructured.Unstructured) (*Inventory, error) { + inventory := NewInventory() - objects, err = exInv.Diff(inv) - if err != nil { + if err := inventory.Add(objects); err != nil { return nil, err } - return objects, nil + return inventory, nil } // Store applies the Inventory object on the server. @@ -71,6 +75,9 @@ func (im *InventoryManager) Store(ctx context.Context, kubeClient client.Client, } cm := im.newConfigMap(name, namespace) + cm.Annotations = map[string]string{ + im.group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), + } cm.Data = map[string]string{ "inventory": string(data), } @@ -105,6 +112,25 @@ func (im *InventoryManager) Retrieve(ctx context.Context, kubeClient client.Clie return &Inventory{Entries: entries}, nil } +// GetStaleObjects returns the list of objects subject to pruning. +func (im *InventoryManager) GetStaleObjects(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + exInv, err := im.Retrieve(ctx, kubeClient, name, namespace) + if err != nil { + if apierrors.IsNotFound(err) { + return objects, nil + } + return nil, err + } + + objects, err = exInv.Diff(inv) + if err != nil { + return nil, err + } + + return objects, nil +} + // Remove deletes the Inventory object from the server. func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client, name, namespace string) error { cm := im.newConfigMap(name, namespace) @@ -117,17 +143,6 @@ func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client return nil } -// Record creates an Inventory of the given objects. -func (im *InventoryManager) Record(objects []*unstructured.Unstructured) (*Inventory, error) { - inventory := NewInventory() - - if err := inventory.Add(objects); err != nil { - return nil, err - } - - return inventory, nil -} - // Read decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. func (im *InventoryManager) Read(r io.Reader) (*unstructured.Unstructured, error) { reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) @@ -217,6 +232,11 @@ func (im *InventoryManager) newConfigMap(name, namespace string) *corev1.ConfigM ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": name, + "app.kubernetes.io/component": "inventory", + "app.kubernetes.io/created-by": im.fieldOwner, + }, }, } } From de9193bc11865b05ff7beb0628009a2895d1d83f Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 14:16:16 +0300 Subject: [PATCH 10/40] Global inventory manager Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 16 +++++++--------- cmd/kustomizer/delete.go | 8 +++----- cmd/kustomizer/main.go | 11 +++++++++++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index e04b0f8..8d8d031 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -29,7 +29,6 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/resmgr" ) @@ -66,7 +65,6 @@ func init() { } func runApplyCmd(cmd *cobra.Command, args []string) error { - invMgr := inventory.NewInventoryManager(PROJECT) objects := make([]*unstructured.Unstructured, 0) if applyArgs.kustomize != "" { @@ -75,7 +73,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return err } - objs, err := invMgr.ReadAll(bytes.NewReader(data)) + objs, err := inventoryMgr.ReadAll(bytes.NewReader(data)) if err != nil { return fmt.Errorf("%s: %w", applyArgs.kustomize, err) } @@ -95,7 +93,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return err } - objs, err := invMgr.ReadAll(bufio.NewReader(ms)) + objs, err := inventoryMgr.ReadAll(bufio.NewReader(ms)) ms.Close() if err != nil { return fmt.Errorf("%s: %w", manifest, err) @@ -108,14 +106,14 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { if applyArgs.output != "" { switch applyArgs.output { case "yaml": - yml, err := invMgr.ToYAML(objects) + yml, err := inventoryMgr.ToYAML(objects) if err != nil { return err } fmt.Println(yml) return nil case "json": - json, err := invMgr.ToJSON(objects) + json, err := inventoryMgr.ToJSON(objects) if err != nil { return err } @@ -133,7 +131,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("--inventory-namespace is required") } - newInventory, err := invMgr.Record(objects) + newInventory, err := inventoryMgr.Record(objects) if err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } @@ -154,12 +152,12 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { fmt.Println(change.String()) } - staleObjects, err := invMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } - err = invMgr.Store(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + err = inventoryMgr.Store(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) if err != nil { return fmt.Errorf("inventory apply failed, error: %w", err) } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index d3af4ce..5c8068f 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -23,13 +23,12 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/resmgr" ) var deleteCmd = &cobra.Command{ Use: "delete", - Short: "Delete Kubernetes objects previous applied", + Short: "Delete the Kubernetes objects in the inventory", RunE: deleteCmdRun, } @@ -57,7 +56,6 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("--inventory-namespace is required") } - invMgr := inventory.NewInventoryManager(PROJECT) resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) if err != nil { return err @@ -66,7 +64,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - inv, err := invMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + inv, err := inventoryMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) objects, err := inv.List() if err != nil { return err @@ -80,7 +78,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { fmt.Println(change.String()) } - err = invMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + err = inventoryMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) if err != nil { return err } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index 4f19d1e..0867698 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -26,6 +26,8 @@ import ( "github.com/spf13/cobra" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/klog/v2" + + "github.com/stefanprodan/kustomizer/pkg/inventory" ) var VERSION = "1.0.0-dev.0" @@ -57,11 +59,20 @@ func init() { rootCmd.DisableAutoGenTag = true } +var inventoryMgr *inventory.InventoryManager + func main() { klog.InitFlags(nil) flag.Parse() configureKubeconfig() + + if im, err := inventory.NewInventoryManager(PROJECT, PROJECT+".dev"); err != nil { + panic(err) + } else { + inventoryMgr = im + } + if err := rootCmd.Execute(); err != nil { klog.Errorf("%v", err) os.Exit(1) From cda56e7559b76ac1e669a1f4d4e3ca0d2ad6ba6f Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 15:00:38 +0300 Subject: [PATCH 11/40] Implement build command Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 82 +++------------------ cmd/kustomizer/{utils.go => build.go} | 101 +++++++++++++++++++++++++- cmd/kustomizer/delete.go | 9 +-- cmd/kustomizer/log.go | 30 ++++++++ cmd/kustomizer/main.go | 19 ++--- go.mod | 2 +- 6 files changed, 152 insertions(+), 91 deletions(-) rename cmd/kustomizer/{utils.go => build.go} (51%) create mode 100644 cmd/kustomizer/log.go diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 8d8d031..017f561 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -1,6 +1,5 @@ /* Copyright 2021 Stefan Prodan -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. @@ -18,17 +17,11 @@ limitations under the License. package main import ( - "bufio" - "bytes" "context" "fmt" - "os" - "sort" "time" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/stefanprodan/kustomizer/pkg/resmgr" ) @@ -41,7 +34,6 @@ var applyCmd = &cobra.Command{ type applyFlags struct { filename []string kustomize string - output string inventoryName string inventoryNamespace string wait bool @@ -57,7 +49,6 @@ func init() { applyCmd.Flags().BoolVar(&applyArgs.wait, "wait", false, "wait for the applied Kubernetes objects to become ready") applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "recreate objects that contain immutable fields changes") applyCmd.Flags().BoolVar(&applyArgs.prune, "prune", false, "delete stale objects") - applyCmd.Flags().StringVarP(&applyArgs.output, "output", "o", "", "output can be yaml or json") applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "inventory configmap name") applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "inventory configmap namespace") @@ -65,65 +56,9 @@ func init() { } func runApplyCmd(cmd *cobra.Command, args []string) error { - objects := make([]*unstructured.Unstructured, 0) - - if applyArgs.kustomize != "" { - data, err := buildKustomization(applyArgs.kustomize) - if err != nil { - return err - } - - objs, err := inventoryMgr.ReadAll(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("%s: %w", applyArgs.kustomize, err) - } - objects = append(objects, objs...) - } else { - if len(applyArgs.filename) == 0 { - return fmt.Errorf("-f or -k is required") - } - - manifests, err := scan(applyArgs.filename) - if err != nil { - return err - } - for _, manifest := range manifests { - ms, err := os.Open(manifest) - if err != nil { - return err - } - - objs, err := inventoryMgr.ReadAll(bufio.NewReader(ms)) - ms.Close() - if err != nil { - return fmt.Errorf("%s: %w", manifest, err) - } - objects = append(objects, objs...) - } + if applyArgs.kustomize == "" && len(applyArgs.filename) == 0 { + return fmt.Errorf("-f or -k is required") } - - sort.Sort(resmgr.ApplyOrder(objects)) - if applyArgs.output != "" { - switch applyArgs.output { - case "yaml": - yml, err := inventoryMgr.ToYAML(objects) - if err != nil { - return err - } - fmt.Println(yml) - return nil - case "json": - json, err := inventoryMgr.ToJSON(objects) - if err != nil { - return err - } - fmt.Println(json) - return nil - default: - return fmt.Errorf("unsupported output, can be yaml or json") - } - } - if applyArgs.inventoryName == "" { return fmt.Errorf("--inventory-name is required") } @@ -131,6 +66,11 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("--inventory-namespace is required") } + objects, err := buildManifests(applyArgs.kustomize, applyArgs.filename) + if err != nil { + return err + } + newInventory, err := inventoryMgr.Record(objects) if err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) @@ -149,7 +89,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(change.String()) + logger.Println(change.String()) } staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) @@ -168,12 +108,12 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("prune failed, error: %w", err) } for _, change := range changeSet.Entries { - fmt.Println(change.String()) + logger.Println(change.String()) } } if applyArgs.wait { - fmt.Println("waiting for resources to become ready...") + logger.Println("waiting for resources to become ready...") err = resMgr.Wait(objects, 2*time.Second, rootArgs.timeout) if err != nil { @@ -187,7 +127,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { } } - fmt.Println("all resources are ready") + logger.Println("all resources are ready") } return nil diff --git a/cmd/kustomizer/utils.go b/cmd/kustomizer/build.go similarity index 51% rename from cmd/kustomizer/utils.go rename to cmd/kustomizer/build.go index 235f8de..7daf076 100644 --- a/cmd/kustomizer/utils.go +++ b/cmd/kustomizer/build.go @@ -1,6 +1,5 @@ /* Copyright 2021 Stefan Prodan -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. @@ -18,17 +17,115 @@ limitations under the License. package main import ( + "bufio" + "bytes" "fmt" "os" "path" "path/filepath" + "sort" "sync" - "sigs.k8s.io/kustomize/api/filesys" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/kustomize/api/krusty" kustypes "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/filesys" + + "github.com/stefanprodan/kustomizer/pkg/resmgr" ) +var buildCmd = &cobra.Command{ + Use: "build", + Short: "Build a set of Kubernetes manifests or Kustomize overlays.", + RunE: runBuildCmd, +} + +type buildFlags struct { + filename []string + kustomize string + output string +} + +var buildArgs buildFlags + +func init() { + buildCmd.Flags().StringSliceVarP(&buildArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") + buildCmd.Flags().StringVarP(&buildArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") + buildCmd.Flags().StringVarP(&buildArgs.output, "output", "o", "yaml", "output can be yaml or json") + + rootCmd.AddCommand(buildCmd) +} + +func runBuildCmd(cmd *cobra.Command, args []string) error { + if buildArgs.kustomize == "" && len(buildArgs.filename) == 0 { + return fmt.Errorf("-f or -k is required") + } + + objects, err := buildManifests(buildArgs.kustomize, buildArgs.filename) + if err != nil { + return err + } + + switch buildArgs.output { + case "yaml": + yml, err := inventoryMgr.ToYAML(objects) + if err != nil { + return err + } + fmt.Println(yml) + case "json": + json, err := inventoryMgr.ToJSON(objects) + if err != nil { + return err + } + fmt.Println(json) + default: + return fmt.Errorf("unsupported output, can be yaml or json") + } + + return nil +} + +func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + if kustomizePath != "" { + data, err := buildKustomization(kustomizePath) + if err != nil { + return nil, err + } + + objs, err := inventoryMgr.ReadAll(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("%s: %w", kustomizePath, err) + } + objects = append(objects, objs...) + } + + if len(filePaths) > 0 { + manifests, err := scan(filePaths) + if err != nil { + return nil, err + } + for _, manifest := range manifests { + ms, err := os.Open(manifest) + if err != nil { + return nil, err + } + + objs, err := inventoryMgr.ReadAll(bufio.NewReader(ms)) + ms.Close() + if err != nil { + return nil, fmt.Errorf("%s: %w", manifest, err) + } + objects = append(objects, objs...) + } + } + + sort.Sort(resmgr.ApplyOrder(objects)) + return objects, nil +} + func scan(paths []string) ([]string, error) { var manifests []string diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 5c8068f..525ad39 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -1,6 +1,5 @@ /* Copyright 2021 Stefan Prodan -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. @@ -75,7 +74,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return err } for _, change := range changeSet.Entries { - fmt.Println(change.String()) + logger.Println(change.String()) } err = inventoryMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) @@ -83,15 +82,15 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return err } - fmt.Println(fmt.Sprintf("ConfigMap/%s/%s deleted", deleteArgs.inventoryNamespace, deleteArgs.inventoryName)) + logger.Println(fmt.Sprintf("ConfigMap/%s/%s deleted", deleteArgs.inventoryNamespace, deleteArgs.inventoryName)) if deleteArgs.wait { - fmt.Println("waiting for resources to be terminated...") + logger.Println("waiting for resources to be terminated...") err = resMgr.WaitForTermination(objects, 2*time.Second, rootArgs.timeout) if err != nil { return err } - fmt.Println("all resources have been deleted") + logger.Println("all resources have been deleted") } return nil diff --git a/cmd/kustomizer/log.go b/cmd/kustomizer/log.go new file mode 100644 index 0000000..f608c01 --- /dev/null +++ b/cmd/kustomizer/log.go @@ -0,0 +1,30 @@ +/* +Copyright 2021 Stefan Prodan + +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" + "io" +) + +type stderrLogger struct { + stderr io.Writer +} + +func (l stderrLogger) Println(a ...interface{}) { + fmt.Fprintln(l.stderr, a...) +} diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index 0867698..be66504 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -1,6 +1,5 @@ /* Copyright 2021 Stefan Prodan -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. @@ -18,16 +17,13 @@ limitations under the License. package main import ( - "flag" "os" "path/filepath" "time" "github.com/spf13/cobra" - _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/klog/v2" - "github.com/stefanprodan/kustomizer/pkg/inventory" + _ "k8s.io/client-go/plugin/pkg/client/auth" ) var VERSION = "1.0.0-dev.0" @@ -48,7 +44,11 @@ type rootFlags struct { timeout time.Duration } -var rootArgs = rootFlags{} +var ( + rootArgs = rootFlags{} + logger = stderrLogger{stderr: os.Stderr} + inventoryMgr *inventory.InventoryManager +) func init() { rootCmd.PersistentFlags().StringVarP(&rootArgs.kubeconfig, "kubeconfig", "", "", @@ -59,12 +59,7 @@ func init() { rootCmd.DisableAutoGenTag = true } -var inventoryMgr *inventory.InventoryManager - func main() { - klog.InitFlags(nil) - flag.Parse() - configureKubeconfig() if im, err := inventory.NewInventoryManager(PROJECT, PROJECT+".dev"); err != nil { @@ -74,7 +69,7 @@ func main() { } if err := rootCmd.Execute(); err != nil { - klog.Errorf("%v", err) + logger.Println(err) os.Exit(1) } } diff --git a/go.mod b/go.mod index 129acce..0b1ecd3 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( k8s.io/apiextensions-apiserver v0.21.3 k8s.io/apimachinery v0.21.3 k8s.io/client-go v0.21.3 - k8s.io/klog/v2 v2.9.0 sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a sigs.k8s.io/controller-runtime v0.9.6 sigs.k8s.io/kustomize/api v0.9.0 + sigs.k8s.io/kustomize/kyaml v0.11.1 sigs.k8s.io/yaml v1.2.0 ) From 1e970e61b1bfe4734667cf50f26fb3c9c4288426 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 31 Aug 2021 16:23:27 +0300 Subject: [PATCH 12/40] Add e2e test for kustomize overlays pruning Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 14 +++++--- cmd/kustomizer/apply.go | 17 ++++++---- cmd/kustomizer/build.go | 13 +++++--- cmd/kustomizer/delete.go | 8 ++--- cmd/kustomizer/main.go | 8 +++-- testdata/crds/resources/cr.yaml | 7 ---- testdata/kustomize/cr.yaml | 16 +++++++++ .../{crds/definitions => kustomize}/crd.yaml | 7 ++-- testdata/kustomize/kustomization.yaml | 14 ++++++++ testdata/kustomize/kustomizeconfig.yaml | 6 ++++ testdata/{sops => kustomize}/namespace.yaml | 2 +- testdata/kustomize/test.conf | 1 + testdata/sops/kustomization.yaml | 6 ---- testdata/sops/secret.yaml | 33 ------------------- 14 files changed, 80 insertions(+), 72 deletions(-) delete mode 100644 testdata/crds/resources/cr.yaml create mode 100644 testdata/kustomize/cr.yaml rename testdata/{crds/definitions => kustomize}/crd.yaml (78%) create mode 100644 testdata/kustomize/kustomization.yaml create mode 100644 testdata/kustomize/kustomizeconfig.yaml rename testdata/{sops => kustomize}/namespace.yaml (63%) create mode 100644 testdata/kustomize/test.conf delete mode 100644 testdata/sops/kustomization.yaml delete mode 100644 testdata/sops/secret.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8ba446b..c998c47 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -38,19 +38,25 @@ jobs: fi - name: Build run: make build - - name: Apply test + - name: Test apply --kustomize + run: | + ./bin/kustomizer apply -i kustomizer-test -k testdata/kustomize/ --wait --prune + kubectl -n kustomizer-test get tests 2>&1 | grep custom-resource-test1 + - name: Test apply --filename run: | ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune - kubectl -n kustomizer-demo get svc frontend 2>&1 | grep frontend - - name: Prune test + kubectl -n kustomizer-test get svc frontend 2>&1 | grep frontend + - name: Test apply --prune run: | rm -rf testdata/plain/frontend ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune kubectl -n kustomizer-demo get svc frontend 2>&1 | grep NotFound - - name: Delete test + - name: Test delete --wait run: | ./bin/kustomizer delete -i kustomizer-demo --wait kubectl get ns kustomizer-demo 2>&1 | grep NotFound + ./bin/kustomizer delete -i kustomizer-test --wait + kubectl get crd tests.testing.kustomizer.dev 2>&1 | grep NotFound - name: Debug failure if: failure() run: | diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 017f561..f58381d 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -44,13 +44,16 @@ type applyFlags struct { var applyArgs applyFlags func init() { - applyCmd.Flags().StringSliceVarP(&applyArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") - applyCmd.Flags().StringVarP(&applyArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") - applyCmd.Flags().BoolVar(&applyArgs.wait, "wait", false, "wait for the applied Kubernetes objects to become ready") - applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "recreate objects that contain immutable fields changes") - applyCmd.Flags().BoolVar(&applyArgs.prune, "prune", false, "delete stale objects") - applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "inventory configmap name") - applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "inventory configmap namespace") + applyCmd.Flags().StringSliceVarP(&applyArgs.filename, "filename", "f", nil, + "Path to Kubernetes manifest(s). If a directory is specified, then all manifests in the directory tree will be processed recursively.") + applyCmd.Flags().StringVarP(&applyArgs.kustomize, "kustomize", "k", "", + "Path to a directory that contains a kustomization.yaml.") + applyCmd.Flags().BoolVar(&applyArgs.wait, "wait", false, "Wait for the applied Kubernetes objects to become ready.") + applyCmd.Flags().BoolVar(&applyArgs.force, "force", false, "Recreate objects that contain immutable fields changes.") + applyCmd.Flags().BoolVar(&applyArgs.prune, "prune", false, "Delete stale objects from the cluster.") + applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") + applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", + "The namespace of the inventory configmap. The namespace must exist on the target cluster.") rootCmd.AddCommand(applyCmd) } diff --git a/cmd/kustomizer/build.go b/cmd/kustomizer/build.go index 7daf076..0b211fd 100644 --- a/cmd/kustomizer/build.go +++ b/cmd/kustomizer/build.go @@ -50,9 +50,12 @@ type buildFlags struct { var buildArgs buildFlags func init() { - buildCmd.Flags().StringSliceVarP(&buildArgs.filename, "filename", "f", nil, "path to Kubernetes manifest(s)") - buildCmd.Flags().StringVarP(&buildArgs.kustomize, "kustomize", "k", "", "process a kustomization directory (can't be used together with -f)") - buildCmd.Flags().StringVarP(&buildArgs.output, "output", "o", "yaml", "output can be yaml or json") + buildCmd.Flags().StringSliceVarP(&buildArgs.filename, "filename", "f", nil, + "Path to Kubernetes manifest(s). If a directory is specified, then all manifests in the directory tree will be processed recursively.") + buildCmd.Flags().StringVarP(&buildArgs.kustomize, "kustomize", "k", "", + "Path to a directory that contains a kustomization.yaml.") + buildCmd.Flags().StringVarP(&buildArgs.output, "output", "o", "yaml", + "Write manifests to stdout in YAML or JSON format.") rootCmd.AddCommand(buildCmd) } @@ -103,7 +106,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U } if len(filePaths) > 0 { - manifests, err := scan(filePaths) + manifests, err := scanForManifests(filePaths) if err != nil { return nil, err } @@ -126,7 +129,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U return objects, nil } -func scan(paths []string) ([]string, error) { +func scanForManifests(paths []string) ([]string, error) { var manifests []string for _, in := range paths { diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 525ad39..46559fd 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -27,7 +27,7 @@ import ( var deleteCmd = &cobra.Command{ Use: "delete", - Short: "Delete the Kubernetes objects in the inventory", + Short: "Delete the Kubernetes objects in the inventory.", RunE: deleteCmdRun, } @@ -40,9 +40,9 @@ type deleteFlags struct { var deleteArgs deleteFlags func init() { - deleteCmd.Flags().StringVarP(&deleteArgs.inventoryName, "inventory-name", "i", "", "inventory configmap name") - deleteCmd.Flags().StringVar(&deleteArgs.inventoryNamespace, "inventory-namespace", "default", "inventory configmap namespace") - deleteCmd.Flags().BoolVar(&deleteArgs.wait, "wait", true, "wait for the deleted Kubernetes objects to be terminated") + deleteCmd.Flags().StringVarP(&deleteArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") + deleteCmd.Flags().StringVar(&deleteArgs.inventoryNamespace, "inventory-namespace", "default", "The namespace of the inventory configmap.") + deleteCmd.Flags().BoolVar(&deleteArgs.wait, "wait", true, "Wait for the deleted Kubernetes objects to be terminated.") rootCmd.AddCommand(deleteCmd) } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index be66504..b1b80e8 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -52,9 +52,11 @@ var ( func init() { rootCmd.PersistentFlags().StringVarP(&rootArgs.kubeconfig, "kubeconfig", "", "", - "absolute path to the kubeconfig file") - rootCmd.PersistentFlags().StringVarP(&rootArgs.kubecontext, "context", "", "", "kubernetes context to use") - rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", time.Minute, "timeout for this operation") + "Absolute path to the kubeconfig file.") + rootCmd.PersistentFlags().StringVarP(&rootArgs.kubecontext, "context", "", "", + "The Kubernetes context to use.") + rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", time.Minute, + "The length of time to wait before giving up on the current operation.") rootCmd.DisableAutoGenTag = true } diff --git a/testdata/crds/resources/cr.yaml b/testdata/crds/resources/cr.yaml deleted file mode 100644 index 82370e2..0000000 --- a/testdata/crds/resources/cr.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: testing.k8s.io/v1 -kind: Test -metadata: - name: some-test - namespace: default -spec: - type: integration diff --git a/testdata/kustomize/cr.yaml b/testdata/kustomize/cr.yaml new file mode 100644 index 0000000..c90cead --- /dev/null +++ b/testdata/kustomize/cr.yaml @@ -0,0 +1,16 @@ +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test1 +spec: + type: integration + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test2 +spec: + type: unit + valuesFrom: test-config +--- diff --git a/testdata/crds/definitions/crd.yaml b/testdata/kustomize/crd.yaml similarity index 78% rename from testdata/crds/definitions/crd.yaml rename to testdata/kustomize/crd.yaml index fd92a83..d12e0da 100644 --- a/testdata/crds/definitions/crd.yaml +++ b/testdata/kustomize/crd.yaml @@ -1,9 +1,9 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: tests.testing.k8s.io + name: tests.testing.kustomizer.dev spec: - group: testing.k8s.io + group: testing.kustomizer.dev version: v1 versions: - name: v1 @@ -31,3 +31,6 @@ spec: enum: - unit - integration + valuesFrom: + description: ConfigMap reference + type: string diff --git a/testdata/kustomize/kustomization.yaml b/testdata/kustomize/kustomization.yaml new file mode 100644 index 0000000..74565b7 --- /dev/null +++ b/testdata/kustomize/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test +configurations: + - kustomizeconfig.yaml +resources: +- namespace.yaml +- crd.yaml +- cr.yaml +configMapGenerator: + - name: test-config + files: + - test.conf + diff --git a/testdata/kustomize/kustomizeconfig.yaml b/testdata/kustomize/kustomizeconfig.yaml new file mode 100644 index 0000000..a5b0487 --- /dev/null +++ b/testdata/kustomize/kustomizeconfig.yaml @@ -0,0 +1,6 @@ +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom + kind: Test diff --git a/testdata/sops/namespace.yaml b/testdata/kustomize/namespace.yaml similarity index 63% rename from testdata/sops/namespace.yaml rename to testdata/kustomize/namespace.yaml index de51780..e5f0515 100644 --- a/testdata/sops/namespace.yaml +++ b/testdata/kustomize/namespace.yaml @@ -1,4 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: test2 + name: kustomizer-test diff --git a/testdata/kustomize/test.conf b/testdata/kustomize/test.conf new file mode 100644 index 0000000..8462a7a --- /dev/null +++ b/testdata/kustomize/test.conf @@ -0,0 +1 @@ +test=test2 \ No newline at end of file diff --git a/testdata/sops/kustomization.yaml b/testdata/sops/kustomization.yaml deleted file mode 100644 index b19345e..0000000 --- a/testdata/sops/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: test2 -resources: - - secret.yaml - - namespace.yaml diff --git a/testdata/sops/secret.yaml b/testdata/sops/secret.yaml deleted file mode 100644 index 58ff518..0000000 --- a/testdata/sops/secret.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: v1 -data: - password: ENC[AES256_GCM,data:kxrD5xoC2t0=,iv:w2AtSHOgqEeKICYG70LwNwsy0HraAujhfBeabqp0O4c=,tag:PkO08GLLebzYAxF4eJUiqA==,type:str] -kind: Secret -metadata: - name: test - annotations: - test: "2" -sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - lastmodified: '2020-08-22T08:00:58Z' - mac: ENC[AES256_GCM,data:8PhS4AALpJKxnFQn6eGRl8um8KN7qh/mem6jRpkkEuXB4wwYrUyLV9ClYoSdmTULrSHsW8m3A1EpKby67bmDuUcMTzX2TDbWb50d6ivBbBmTsQvW22hI1/UPstiM3r3Or8gGIkaj8NrjLjHwLQV4cPcsk0HZ4vFn8b6+un1EAj0=,iv:ToObhfB5r0Llr1XhVLtasNJ+xIdNYTcLnnTd7Zg72ts=,tag:Lr/A05EjvDT3qVwo3k/RoQ==,type:str] - pgp: - - created_at: '2020-08-22T08:00:57Z' - enc: | - -----BEGIN PGP MESSAGE----- - - hQEMAyUpShfNkFB/AQf+OXrPlyj098j3iAjG+jPTzZnUhtRjSermp02sOubRj2/8 - KABcmcWmQwoDDkIs2snnioHZhfpmzWCfA91Rhz/72KA0UKtGT149JEiAQ5R3I0y7 - zR4+cn7kqeTpZk7WLZJWO8NQZxzm8bjZjY6vTfbZk7JlJWfwv/IIFM8KTarWZbgm - RcgY/iF/ei1lPQR8ioiC8UJE9XjuR8SEi1oyt5Rc30DvloYK03monAJExqleN3RB - UBohdzvbTpYPwH+fPFj7L+tb8n11L4pzGBjoVSBkGHDnDqKYs9YLo6Zh5YoN8Fxh - 3nYV5rXrNZ5jcskyPbUwgx3Gkx+MUqUPStQ7jo0X1NJeAYb9QISU+w3kb3DcY2X2 - mDTj0JeX7ulyMOx/4WdvTtaWeQbh9ERtuuWbd3i6yBBU+rxGSnhgXbo2wHisAN4S - aynag7ZrmNz2X+zE+g0pGsqevPSK+XUXJcg3YE0JHg== - =HCsv - -----END PGP MESSAGE----- - fp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 - encrypted_regex: ^(data|stringData)$ - version: 3.6.0 From 67c386dde4360cb85d872cb5ff3c843ad14a6b10 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 10:54:07 +0300 Subject: [PATCH 13/40] Apply mutating and validating webhooks last Signed-off-by: Stefan Prodan --- cmd/kustomizer/delete.go | 4 ++++ pkg/inventory/sort.go | 7 +------ pkg/resmgr/sort.go | 35 +++++++++++++++++------------------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 46559fd..41fca14 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -64,6 +64,10 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { defer cancel() inv, err := inventoryMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + if err != nil { + return err + } + objects, err := inv.List() if err != nil { return err diff --git a/pkg/inventory/sort.go b/pkg/inventory/sort.go index 7659b4f..73f725b 100644 --- a/pkg/inventory/sort.go +++ b/pkg/inventory/sort.go @@ -50,16 +50,11 @@ func (objects InventoryOrder) Less(i, j int) bool { // in the partial ordering of Kubernetes resources. func rankOfKind(kind string) int { switch strings.ToLower(kind) { - // API extensions case "customresourcedefinition": return 0 - // Namespace objects case "namespace": return 1 - // Global objects - case "clusterrole", "clusterrolebinding", "ingressclass", "storageclass": - return 2 default: - return 3 + return 2 } } diff --git a/pkg/resmgr/sort.go b/pkg/resmgr/sort.go index 529b480..01ace0b 100644 --- a/pkg/resmgr/sort.go +++ b/pkg/resmgr/sort.go @@ -23,7 +23,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -// ApplyOrder implements the Sort interface for unstructured objects. +// ApplyOrder implements the Sort interface for Kubernetes objects based on kind. +// When creating objects: CRDs, namespaces and other global kinds go first, while webhooks go last. +// When deleting objects: the order is inverted to allow the Kubernetes controllers to finalize custom resources. type ApplyOrder []*unstructured.Unstructured func (objects ApplyOrder) Len() int { @@ -39,34 +41,31 @@ func (objects ApplyOrder) Less(i, j int) bool { ni := objects[i].GetName() kj := objects[j].GetKind() nj := objects[j].GetName() - ranki, rankj := rankOfKind(ki), rankOfKind(kj) + ranki, rankj := RankOfKind(ki), RankOfKind(kj) if ranki == rankj { return ni < nj } return ranki < rankj } -// rankOfKind returns an int denoting the position of the given kind -// in the partial ordering of Kubernetes resources, according to which -// kinds depend on which (derived by hand). -func rankOfKind(kind string) int { +// RankOfKind returns an int denoting the position of the given kind in the partial ordering of Kubernetes resources. +func RankOfKind(kind string) int { switch strings.ToLower(kind) { - // API extensions - case "customresourcedefinition": + case "customresourcedefinition", "apiservice": return 0 - // Global objects - case "namespace", "clusterrolebinding", "clusterrole": + case "namespace": return 1 - // Namespaced objects - case "serviceaccount", "role", "rolebinding", "service", "endpoint", "ingress": + case "clusterrole", "clusterrolebinding", "ingressclass", "runtimeclass", "storageclass", "priorityclass", "certificatesigningrequest", "podsecuritypolicy": return 2 - // Namespaced objects - case "resourcequota", "limitrange", "secret", "configmap", "persistentvolume", "persistentvolumeclaim": - return 2 - // Workload objects - case "daemonset", "deployment", "job", "cronjob", "statefulset", "replicationcontroller", "replicaset", "pod": + case "secret", "configmap", "lease", "serviceaccount", "role", "rolebinding", "service", "endpoint", "endpointslice", "ingress", "networkpolicy": return 3 - default: + case "resourcequota", "limitrange", "podpreset", "persistentvolume", "persistentvolumeclaim", "poddisruptionbudget", "horizontalpodautoscaler": return 4 + case "daemonset", "deployment", "job", "cronjob", "statefulset", "replicationcontroller", "replicaset", "pod": + return 5 + default: + return 6 + case "mutatingwebhookconfiguration", "validatingwebhookconfiguration": + return 7 } } From 97bcdc39f690069d6d0b24a269ff3d902207b77d Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 10:55:57 +0300 Subject: [PATCH 14/40] Update controller-runtime to v0.10.0 Signed-off-by: Stefan Prodan --- go.mod | 10 ++-- go.sum | 164 ++++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 137 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index 0b1ecd3..013c886 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,12 @@ go 1.16 require ( github.com/spf13/cobra v1.1.3 - k8s.io/api v0.21.3 - k8s.io/apiextensions-apiserver v0.21.3 - k8s.io/apimachinery v0.21.3 - k8s.io/client-go v0.21.3 + k8s.io/api v0.22.1 + k8s.io/apiextensions-apiserver v0.22.1 + k8s.io/apimachinery v0.22.1 + k8s.io/client-go v0.22.1 sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a - sigs.k8s.io/controller-runtime v0.9.6 + sigs.k8s.io/controller-runtime v0.10.0 sigs.k8s.io/kustomize/api v0.9.0 sigs.k8s.io/kustomize/kyaml v0.11.1 sigs.k8s.io/yaml v1.2.0 diff --git a/go.sum b/go.sum index 028fe58..8059d68 100644 --- a/go.sum +++ b/go.sum @@ -24,18 +24,23 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= +github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= +github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 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= @@ -61,6 +66,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -73,9 +79,11 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= @@ -83,7 +91,11 @@ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -91,7 +103,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= +github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= +github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -101,6 +118,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -128,7 +146,11 @@ github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkg github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -138,14 +160,17 @@ github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQL github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -179,13 +204,15 @@ github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= @@ -207,8 +234,9 @@ github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= @@ -216,10 +244,12 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -228,6 +258,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -247,12 +278,14 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 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/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= 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/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -294,9 +327,11 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -333,6 +368,9 @@ github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -346,6 +384,7 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -369,8 +408,9 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= 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= @@ -378,6 +418,7 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -391,6 +432,7 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -426,6 +468,7 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 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.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= @@ -435,8 +478,8 @@ 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.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.14.0 h1:ep6kpPVwmr/nTbklSx2nrLNSIO62DoYAhnPNIMhK8gI= -github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= @@ -471,12 +514,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -485,6 +530,7 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -493,10 +539,12 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -511,9 +559,11 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -551,6 +601,7 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -561,11 +612,20 @@ github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6Ut github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= +go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= +go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= +go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -575,6 +635,18 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= +go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -593,8 +665,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -631,6 +704,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -640,6 +714,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -672,11 +747,15 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= +golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -691,6 +770,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -728,9 +808,11 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -738,11 +820,14 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= +golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/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 h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -753,6 +838,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -765,6 +851,7 @@ golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -809,11 +896,13 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -853,9 +942,12 @@ google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4 google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -865,9 +957,15 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -903,6 +1001,7 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -926,25 +1025,25 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= -k8s.io/api v0.21.3 h1:cblWILbLO8ar+Fj6xdDGr603HRsf8Wu9E9rngJeprZQ= -k8s.io/api v0.21.3/go.mod h1:hUgeYHUbBp23Ue4qdX9tR8/ANi/g3ehylAqDn9NWVOg= +k8s.io/api v0.22.1 h1:ISu3tD/jRhYfSW8jI/Q1e+lRxkR7w9UwQEZ7FgslrwY= +k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= -k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY= -k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= +k8s.io/apiextensions-apiserver v0.22.1 h1:YSJYzlFNFSfUle+yeEXX0lSQyLEoxoPJySRupepb0gE= +k8s.io/apiextensions-apiserver v0.22.1/go.mod h1:HeGmorjtRmRLE+Q8dJu6AYRoZccvCMsghwS8XTUYb2c= k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= -k8s.io/apimachinery v0.21.3 h1:3Ju4nvjCngxxMYby0BimUk+pQHPOQp3eCGChk5kfVII= -k8s.io/apimachinery v0.21.3/go.mod h1:H/IM+5vH9kZRNJ4l3x/fXP/5bOPJaVP/guptnZPeCFI= +k8s.io/apimachinery v0.22.1 h1:DTARnyzmdHMz7bFWFDDm22AM4pLWTQECMpRTFu2d2OM= +k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= -k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= +k8s.io/apiserver v0.22.1/go.mod h1:2mcM6dzSt+XndzVQJX21Gx0/Klo7Aen7i0Ai6tIa400= k8s.io/cli-runtime v0.21.1 h1:Oj/iZxa7LLXrhzShaLNF4rFJEIEBTDHj0dJw4ra2vX4= k8s.io/cli-runtime v0.21.1/go.mod h1:TI9Bvl8lQWZB2KqE91QLCp9AZE4l29zNFnj/x4IX4Fw= k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= -k8s.io/client-go v0.21.3 h1:J9nxZTOmvkInRDCzcSNQmPJbDYN/PjlxXT9Mos3HcLg= -k8s.io/client-go v0.21.3/go.mod h1:+VPhCgTsaFmGILxR/7E1N0S+ryO010QBeNCv5JwRGYU= +k8s.io/client-go v0.22.1 h1:jW0ZSHi8wW260FvcXHkIa0NLxFBQszTlhiAVsU5mopw= +k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= -k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= +k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= -k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= +k8s.io/component-base v0.22.1/go.mod h1:0D+Bl8rrnsPN9v0dyYvkqFfBeAd4u7n77ze+p8CMiPo= k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -960,18 +1059,19 @@ k8s.io/kubectl v0.21.1/go.mod h1:PMYR88MqESuysBM/MX+Vu4JbX/50nY4d4kny+SPEI2U= k8s.io/metrics v0.21.1/go.mod h1:pyDVLsLe++FIGDBFU80NcW4xMFsuiVTWL8Zfi7+PpNo= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20210517184530-5a248b5acedc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471 h1:DnzUXII7sVg1FJ/4JX6YDRJfLNAC7idRatPwe07suiI= -k8s.io/utils v0.0.0-20210722164352-7f3ee0f31471/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176 h1:Mx0aa+SUAcNRQbs5jUzV8lkDlGFU8laZsY9jrcVX5SY= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.19/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a h1:S17+FPWGsOonXim+GcadLcSblEwL131Y9fKLfifSYkY= sigs.k8s.io/cli-utils v0.25.1-0.20210608181808-f3974341173a/go.mod h1:I4jgHr6uRfX0CkOMECwSgg2J48rNzZE1+kDXj9SnJBc= sigs.k8s.io/controller-runtime v0.9.0-beta.5.0.20210524185538-7181f1162e79/go.mod h1:rgf+cBz72pYlKXDRNhI1WFQv/S86EMUV4/ySmsEYgHk= -sigs.k8s.io/controller-runtime v0.9.6 h1:EevVMlgUj4fC1NVM4+DB3iPkWkmGRNarA66neqv9Qew= -sigs.k8s.io/controller-runtime v0.9.6/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= +sigs.k8s.io/controller-runtime v0.10.0 h1:HgyZmMpjUOrtkaFtCnfxsR1bGRuFoAczSNbn2MoKj5U= +sigs.k8s.io/controller-runtime v0.10.0/go.mod h1:GCdh6kqV6IY4LK0JLwX0Zm6g233RtVGdb/f0+KSfprg= sigs.k8s.io/kustomize/api v0.8.8/go.mod h1:He1zoK0nk43Pc6NlV085xDXDXTNprtcyKZVm3swsdNY= sigs.k8s.io/kustomize/api v0.9.0 h1:yGQnm/2GBbHBLSVbM0CsqgPmqK/NSDbhSGbXuhuVN7s= sigs.k8s.io/kustomize/api v0.9.0/go.mod h1:bOF7z4DcRIXcOCeSbVq5o9JhMRnNzWqrRSSBFtz05A4= From 104e922eb5210c8a5bdbac425eae48a3462e19f3 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 11:36:35 +0300 Subject: [PATCH 15/40] Add load test to CI Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 6 + testdata/loadtest/README.md | 46 ++ testdata/loadtest/base/custom-resources.yaml | 81 ++ testdata/loadtest/base/deployments.yaml | 739 ++++++++++++++++++ testdata/loadtest/base/kustomization.yaml | 11 + testdata/loadtest/base/kustomizeconfig.yaml | 6 + testdata/loadtest/base/test.conf | 1 + testdata/loadtest/crd.yaml | 65 ++ testdata/loadtest/kustomization.yaml | 9 + testdata/loadtest/overlay1/kustomization.yaml | 6 + testdata/loadtest/overlay1/namespace.yaml | 4 + testdata/loadtest/overlay2/kustomization.yaml | 6 + testdata/loadtest/overlay2/namespace.yaml | 4 + testdata/loadtest/overlay3/kustomization.yaml | 6 + testdata/loadtest/overlay3/namespace.yaml | 4 + testdata/loadtest/overlay4/kustomization.yaml | 6 + testdata/loadtest/overlay4/namespace.yaml | 4 + testdata/loadtest/overlay5/kustomization.yaml | 6 + testdata/loadtest/overlay5/namespace.yaml | 4 + 19 files changed, 1014 insertions(+) create mode 100644 testdata/loadtest/README.md create mode 100644 testdata/loadtest/base/custom-resources.yaml create mode 100644 testdata/loadtest/base/deployments.yaml create mode 100644 testdata/loadtest/base/kustomization.yaml create mode 100644 testdata/loadtest/base/kustomizeconfig.yaml create mode 100644 testdata/loadtest/base/test.conf create mode 100644 testdata/loadtest/crd.yaml create mode 100644 testdata/loadtest/kustomization.yaml create mode 100644 testdata/loadtest/overlay1/kustomization.yaml create mode 100644 testdata/loadtest/overlay1/namespace.yaml create mode 100644 testdata/loadtest/overlay2/kustomization.yaml create mode 100644 testdata/loadtest/overlay2/namespace.yaml create mode 100644 testdata/loadtest/overlay3/kustomization.yaml create mode 100644 testdata/loadtest/overlay3/namespace.yaml create mode 100644 testdata/loadtest/overlay4/kustomization.yaml create mode 100644 testdata/loadtest/overlay4/namespace.yaml create mode 100644 testdata/loadtest/overlay5/kustomization.yaml create mode 100644 testdata/loadtest/overlay5/namespace.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c998c47..1ca291d 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -57,6 +57,12 @@ jobs: kubectl get ns kustomizer-demo 2>&1 | grep NotFound ./bin/kustomizer delete -i kustomizer-test --wait kubectl get crd tests.testing.kustomizer.dev 2>&1 | grep NotFound + - name: Load test apply (110 objects) + run: | + time ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test + - name: Load test delete (110 objects) + run: | + time ./bin/kustomizer delete --inventory-name load-test - name: Debug failure if: failure() run: | diff --git a/testdata/loadtest/README.md b/testdata/loadtest/README.md new file mode 100644 index 0000000..3bac85b --- /dev/null +++ b/testdata/loadtest/README.md @@ -0,0 +1,46 @@ +# Load test + +Commands: + +```shell +# count objects +kustomize build ./testdata/loadtest/ --load-restrictor=LoadRestrictionsNone | grep apiVersion | wc -l + +kubectl apply -f apply -f ./testdata/loadtest/crd.yaml + +# kubectl dry-run +time kustomize build ./testdata/loadtest/ --load-restrictor=LoadRestrictionsNone | kubectl apply -f- --dry-run + +# kubectl apply +time kustomize build ./testdata/loadtest/ --load-restrictor=LoadRestrictionsNone | kubectl apply -f- + +# kubectl delete +time kustomize build ./testdata/loadtest/ --load-restrictor=LoadRestrictionsNone --reorder=none | kubectl delete -f- + +# kustomizer dry-run + apply +time ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test + +# kustomizer dry-run +time ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test + +# kustomizer delete +time ./bin/kustomizer delete --inventory-name load-test +``` + +Env: + +- Kubernetes v1.20.8-gke.2100 +- kubectl v1.21.3 + +Results: + +| Operation | Time | Objects | +| ------------------------------------------ | ------------------ | ------------------ | +| kubectl dry-run | 16.024s | 110 | +| kustomizer dry-run | 15.258s | 110 | +| kubectl dry-run + apply | 35.770s | 110 | +| kustomizer dry-run + apply | 25.575s | 110 | +| kubectl delete | 28.486s | 110 | +| kustomizer delete | 21.507s | 110 | + + diff --git a/testdata/loadtest/base/custom-resources.yaml b/testdata/loadtest/base/custom-resources.yaml new file mode 100644 index 0000000..fa19b80 --- /dev/null +++ b/testdata/loadtest/base/custom-resources.yaml @@ -0,0 +1,81 @@ +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test1 +spec: + type: integration + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test2 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test3 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test4 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test5 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test6 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test7 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test8 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test9 +spec: + type: unit + valuesFrom: test-config +--- +apiVersion: testing.kustomizer.dev/v1 +kind: Test +metadata: + name: custom-resource-test10 +spec: + type: unit + valuesFrom: test-config +--- + diff --git a/testdata/loadtest/base/deployments.yaml b/testdata/loadtest/base/deployments.yaml new file mode 100644 index 0000000..4302d7f --- /dev/null +++ b/testdata/loadtest/base/deployments.yaml @@ -0,0 +1,739 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend1 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend1 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend1 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend2 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend2 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend2 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend3 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend3 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend3 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend4 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend4 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend4 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend5 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend5 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend5 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend6 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend6 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend6 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend7 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend7 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend7 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend8 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend8 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend8 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend9 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend9 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend9 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend10 +spec: + replicas: 0 + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: frontend10 + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: frontend10 + spec: + serviceAccountName: demo + containers: + - name: frontend + image: stefanprodan/podinfo:3.2.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 + - --level=info + - --backend-url=http://backend:9898/echo + 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: 1000m + memory: 128Mi + requests: + cpu: 100m + memory: 32Mi \ No newline at end of file diff --git a/testdata/loadtest/base/kustomization.yaml b/testdata/loadtest/base/kustomization.yaml new file mode 100644 index 0000000..c67afdd --- /dev/null +++ b/testdata/loadtest/base/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configurations: + - kustomizeconfig.yaml +resources: +- custom-resources.yaml +- deployments.yaml +configMapGenerator: + - name: test-config + files: + - test.conf diff --git a/testdata/loadtest/base/kustomizeconfig.yaml b/testdata/loadtest/base/kustomizeconfig.yaml new file mode 100644 index 0000000..a5b0487 --- /dev/null +++ b/testdata/loadtest/base/kustomizeconfig.yaml @@ -0,0 +1,6 @@ +nameReference: + - kind: ConfigMap + version: v1 + fieldSpecs: + - path: spec/valuesFrom + kind: Test diff --git a/testdata/loadtest/base/test.conf b/testdata/loadtest/base/test.conf new file mode 100644 index 0000000..8462a7a --- /dev/null +++ b/testdata/loadtest/base/test.conf @@ -0,0 +1 @@ +test=test2 \ No newline at end of file diff --git a/testdata/loadtest/crd.yaml b/testdata/loadtest/crd.yaml new file mode 100644 index 0000000..81dcede --- /dev/null +++ b/testdata/loadtest/crd.yaml @@ -0,0 +1,65 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.5.0 + creationTimestamp: null + name: tests.testing.kustomizer.dev +spec: + group: testing.kustomizer.dev + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.type + name: TYPE + type: string + name: v1 + schema: + openAPIV3Schema: + description: Test is the Schema for the testing API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TestSpec defines the desired state of a test run + properties: + type: + description: Type of test + type: string + enum: + - unit + - integration + valuesFrom: + description: config reference + type: string + type: object + status: + description: BucketStatus defines the observed state of a bucket + properties: + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/testdata/loadtest/kustomization.yaml b/testdata/loadtest/kustomization.yaml new file mode 100644 index 0000000..cf5c6b2 --- /dev/null +++ b/testdata/loadtest/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- crd.yaml +- overlay1 +- overlay2 +- overlay3 +- overlay4 +- overlay5 diff --git a/testdata/loadtest/overlay1/kustomization.yaml b/testdata/loadtest/overlay1/kustomization.yaml new file mode 100644 index 0000000..adb4c9a --- /dev/null +++ b/testdata/loadtest/overlay1/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test1 +resources: +- ../base +- namespace.yaml diff --git a/testdata/loadtest/overlay1/namespace.yaml b/testdata/loadtest/overlay1/namespace.yaml new file mode 100644 index 0000000..142c239 --- /dev/null +++ b/testdata/loadtest/overlay1/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-test1 diff --git a/testdata/loadtest/overlay2/kustomization.yaml b/testdata/loadtest/overlay2/kustomization.yaml new file mode 100644 index 0000000..98f57d8 --- /dev/null +++ b/testdata/loadtest/overlay2/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test2 +resources: + - ../base + - namespace.yaml diff --git a/testdata/loadtest/overlay2/namespace.yaml b/testdata/loadtest/overlay2/namespace.yaml new file mode 100644 index 0000000..4396c9e --- /dev/null +++ b/testdata/loadtest/overlay2/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-test2 diff --git a/testdata/loadtest/overlay3/kustomization.yaml b/testdata/loadtest/overlay3/kustomization.yaml new file mode 100644 index 0000000..cf4354a --- /dev/null +++ b/testdata/loadtest/overlay3/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test3 +resources: + - ../base + - namespace.yaml diff --git a/testdata/loadtest/overlay3/namespace.yaml b/testdata/loadtest/overlay3/namespace.yaml new file mode 100644 index 0000000..7b6724b --- /dev/null +++ b/testdata/loadtest/overlay3/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-test3 diff --git a/testdata/loadtest/overlay4/kustomization.yaml b/testdata/loadtest/overlay4/kustomization.yaml new file mode 100644 index 0000000..e2d2664 --- /dev/null +++ b/testdata/loadtest/overlay4/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test4 +resources: + - ../base + - namespace.yaml diff --git a/testdata/loadtest/overlay4/namespace.yaml b/testdata/loadtest/overlay4/namespace.yaml new file mode 100644 index 0000000..a9a9dd6 --- /dev/null +++ b/testdata/loadtest/overlay4/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-test4 diff --git a/testdata/loadtest/overlay5/kustomization.yaml b/testdata/loadtest/overlay5/kustomization.yaml new file mode 100644 index 0000000..1576ba4 --- /dev/null +++ b/testdata/loadtest/overlay5/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: kustomizer-test5 +resources: + - ../base + - namespace.yaml diff --git a/testdata/loadtest/overlay5/namespace.yaml b/testdata/loadtest/overlay5/namespace.yaml new file mode 100644 index 0000000..ad65430 --- /dev/null +++ b/testdata/loadtest/overlay5/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-test5 From 10c694069f6a1dcd17b05c858c331c0fd4d347e6 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 17:10:58 +0300 Subject: [PATCH 16/40] Implement staged apply for CRDs+CRs Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 33 ++++++++++++++++++++++++---- pkg/resmgr/changeset.go | 4 ++++ pkg/resmgr/manager.go | 48 ++++++++++++++++++++++++++++++++++++++++- pkg/resmgr/sort.go | 8 +++++-- 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index f58381d..584a42b 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -39,6 +39,7 @@ type applyFlags struct { wait bool force bool prune bool + mode string } var applyArgs applyFlags @@ -54,7 +55,8 @@ func init() { applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "The namespace of the inventory configmap. The namespace must exist on the target cluster.") - + applyCmd.Flags().StringVar(&applyArgs.mode, "mode", "Apply", + "The ResourceManager apply method, can be `Apply`, `ApplyAll`, `ApplyAllStaged`.") rootCmd.AddCommand(applyCmd) } @@ -69,6 +71,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("--inventory-namespace is required") } + logger.Println("building inventory...") objects, err := buildManifests(applyArgs.kustomize, applyArgs.filename) if err != nil { return err @@ -78,6 +81,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } + logger.Println(fmt.Sprintf("applying %v manifest(s)...", len(objects))) resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) if err != nil { @@ -87,12 +91,33 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - for _, object := range objects { - change, err := resMgr.Apply(ctx, object, applyArgs.force) + switch applyArgs.mode { + case "Apply": + for _, object := range objects { + change, err := resMgr.Apply(ctx, object, applyArgs.force) + if err != nil { + return err + } + logger.Println(change.String()) + } + case "ApplyAll": + changeSet, err := resMgr.ApplyAll(ctx, objects, applyArgs.force) + if err != nil { + return err + } + for _, change := range changeSet.Entries { + logger.Println(change.String()) + } + case "ApplyAllStaged": + changeSet, err := resMgr.ApplyAllStaged(ctx, objects, applyArgs.force, 30*time.Second) if err != nil { return err } - logger.Println(change.String()) + for _, change := range changeSet.Entries { + logger.Println(change.String()) + } + default: + return fmt.Errorf("mode not supported") } staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) diff --git a/pkg/resmgr/changeset.go b/pkg/resmgr/changeset.go index c6b2e4f..0004b13 100644 --- a/pkg/resmgr/changeset.go +++ b/pkg/resmgr/changeset.go @@ -42,6 +42,10 @@ func (c *ChangeSet) Add(e ChangeSetEntry) { c.Entries = append(c.Entries, e) } +func (c *ChangeSet) AddAll(e []ChangeSetEntry) { + c.Entries = append(c.Entries, e...) +} + // ChangeSetEntry defines the result of an action performed on an object. type ChangeSetEntry struct { // Subject represents the Object ID in the format 'kind/namespace/name'. diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index a51d7b9..d006f36 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -149,6 +149,48 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured return changeSet, nil } +// ApplyAllStaged extracts the CRDs and Namespaces, applies them with ApplyAll, +// waits for CRDs and Namespaces to become ready, then is applies all the other objects. +// This function should be used when the given objects have a mix of custom resource definition and custom resources, +// or a mix of namespace definitions with namespaced objects. +func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstructured.Unstructured, force bool, wait time.Duration) (*ChangeSet, error) { + changeSet := NewChangeSet() + + // contains only CRDs and Namespaces + var stageOne []*unstructured.Unstructured + + // contains all objects except for CRDs and Namespaces + var stageTwo []*unstructured.Unstructured + + for _, u := range objects { + if IsClusterDefinition(u.GetKind()) { + stageOne = append(stageOne, u) + } else { + stageTwo = append(stageTwo, u) + } + } + + if len(stageOne) > 0 { + cs, err := kc.ApplyAll(ctx, stageOne, force) + if err != nil { + return nil, err + } + changeSet.AddAll(cs.Entries) + + if err := kc.Wait(stageOne, 2*time.Second, wait); err != nil { + return nil, err + } + } + + cs, err := kc.ApplyAll(ctx, stageTwo, force) + if err != nil { + return nil, err + } + changeSet.AddAll(cs.Entries) + + return changeSet, nil +} + // DeleteAll deletes the given set of objects. func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { sort.Sort(sort.Reverse(ApplyOrder(objects))) @@ -192,7 +234,7 @@ func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstr // hasDrifted detects changes to metadata labels, metadata annotations and spec. func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { - if dryRunObject.GetResourceVersion() == "" || existingObject == nil { + if dryRunObject.GetResourceVersion() == "" { return true } @@ -208,6 +250,10 @@ func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { return true } + } else if _, ok := existingObject.Object["webhooks"]; ok { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["webhooks"], existingObject.Object["webhooks"]) { + return true + } } else { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { return true diff --git a/pkg/resmgr/sort.go b/pkg/resmgr/sort.go index 01ace0b..631199a 100644 --- a/pkg/resmgr/sort.go +++ b/pkg/resmgr/sort.go @@ -51,11 +51,11 @@ func (objects ApplyOrder) Less(i, j int) bool { // RankOfKind returns an int denoting the position of the given kind in the partial ordering of Kubernetes resources. func RankOfKind(kind string) int { switch strings.ToLower(kind) { - case "customresourcedefinition", "apiservice": + case "customresourcedefinition": return 0 case "namespace": return 1 - case "clusterrole", "clusterrolebinding", "ingressclass", "runtimeclass", "storageclass", "priorityclass", "certificatesigningrequest", "podsecuritypolicy": + case "apiservice", "clusterrole", "clusterrolebinding", "ingressclass", "runtimeclass", "storageclass", "priorityclass", "certificatesigningrequest", "podsecuritypolicy": return 2 case "secret", "configmap", "lease", "serviceaccount", "role", "rolebinding", "service", "endpoint", "endpointslice", "ingress", "networkpolicy": return 3 @@ -69,3 +69,7 @@ func RankOfKind(kind string) int { return 7 } } + +func IsClusterDefinition(kind string) bool { + return RankOfKind(kind) < 2 +} From 8bc5df4c43172f2d3ee6f56990ed414e6f90a3bc Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 17:13:49 +0300 Subject: [PATCH 17/40] Add staged apply e2e test (cert-manager) Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 10 +++++---- testdata/certs/cert.yaml | 37 +++++++++++++++++++++++++++++++ testdata/certs/kustomization.yaml | 5 +++++ 3 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 testdata/certs/cert.yaml create mode 100644 testdata/certs/kustomization.yaml diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 1ca291d..ded3c4d 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -59,13 +59,15 @@ jobs: kubectl get crd tests.testing.kustomizer.dev 2>&1 | grep NotFound - name: Load test apply (110 objects) run: | - time ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test + ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test - name: Load test delete (110 objects) run: | - time ./bin/kustomizer delete --inventory-name load-test + ./bin/kustomizer delete --inventory-name load-test + - name: Test staged apply + run: | + ./bin/kustomizer apply -k ./testdata/certs/ --prune --wait --inventory-name cert-test --mode=ApplyAllStaged + kubectl -n kustomizer-cert-test wait issuers/my-ca-issuer --for=condition=ready --timeout=1m - name: Debug failure if: failure() run: | - kubectl version --client --short kubectl -n default get configmaps -oyaml - kubectl -n kustomizer-demo get all diff --git a/testdata/certs/cert.yaml b/testdata/certs/cert.yaml new file mode 100644 index 0000000..b311011 --- /dev/null +++ b/testdata/certs/cert.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kustomizer-cert-test +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: my-selfsigned-ca + namespace: kustomizer-cert-test +spec: + isCA: true + commonName: my-selfsigned-ca + secretName: root-secret + privateKey: + algorithm: ECDSA + size: 384 + issuerRef: + name: selfsigned-issuer + kind: ClusterIssuer + group: cert-manager.io +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: my-ca-issuer + namespace: kustomizer-cert-test +spec: + ca: + secretName: root-secret diff --git a/testdata/certs/kustomization.yaml b/testdata/certs/kustomization.yaml new file mode 100644 index 0000000..f72285a --- /dev/null +++ b/testdata/certs/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- https://github.com/jetstack/cert-manager/releases/download/v1.5.3/cert-manager.yaml +- cert.yaml From b0755c246c52464f876ccf403cae86b34060c87a Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 1 Sep 2021 22:10:07 +0300 Subject: [PATCH 18/40] Implement diff command Signed-off-by: Stefan Prodan --- cmd/kustomizer/diff.go | 100 ++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + pkg/resmgr/changeset.go | 2 + pkg/resmgr/manager.go | 62 +++++++++++++++++-------- 4 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 cmd/kustomizer/diff.go diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go new file mode 100644 index 0000000..525e861 --- /dev/null +++ b/cmd/kustomizer/diff.go @@ -0,0 +1,100 @@ +/* +Copyright 2021 Stefan Prodan + +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" + "fmt" + "github.com/spf13/cobra" + "github.com/stefanprodan/kustomizer/pkg/resmgr" +) + +var diffCmd = &cobra.Command{ + Use: "diff", + Short: "Diff Kubernetes manifests and Kustomize overlays using server-side dry-run.", + RunE: runDiffCmd, +} + +type diffFlags struct { + filename []string + kustomize string + inventoryName string + inventoryNamespace string + prune bool +} + +var diffArgs diffFlags + +func init() { + diffCmd.Flags().StringSliceVarP(&diffArgs.filename, "filename", "f", nil, + "Path to Kubernetes manifest(s). If a directory is specified, then all manifests in the directory tree will be processed recursively.") + diffCmd.Flags().StringVarP(&diffArgs.kustomize, "kustomize", "k", "", + "Path to a directory that contains a kustomization.yaml.") + diffCmd.Flags().BoolVar(&diffArgs.prune, "prune", false, "Delete stale objects from the cluster.") + diffCmd.Flags().StringVarP(&diffArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") + diffCmd.Flags().StringVar(&diffArgs.inventoryNamespace, "inventory-namespace", "default", + "The namespace of the inventory configmap. The namespace must exist on the target cluster.") + rootCmd.AddCommand(diffCmd) +} + +func runDiffCmd(cmd *cobra.Command, args []string) error { + if diffArgs.kustomize == "" && len(diffArgs.filename) == 0 { + return fmt.Errorf("-f or -k is required") + } + + objects, err := buildManifests(diffArgs.kustomize, diffArgs.filename) + if err != nil { + return err + } + + newInventory, err := inventoryMgr.Record(objects) + if err != nil { + return fmt.Errorf("creating inventory failed, error: %w", err) + } + + resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + for _, object := range objects { + change, err := resMgr.Diff(ctx, object) + if err != nil { + return err + } + if change.Diff != "" { + logger.Println(change.String()) + logger.Println(change.Diff) + } + } + + if diffArgs.inventoryName != "" { + + staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, diffArgs.inventoryName, diffArgs.inventoryNamespace) + if err != nil { + return fmt.Errorf("inventory query failed, error: %w", err) + } + + for _, object := range staleObjects { + logger.Println(fmt.Sprintf("%s/%s/%s deleted", object.GetKind(), object.GetNamespace(), object.GetName())) + } + } + return nil +} diff --git a/go.mod b/go.mod index 013c886..0fcdca2 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stefanprodan/kustomizer go 1.16 require ( + github.com/google/go-cmp v0.5.5 github.com/spf13/cobra v1.1.3 k8s.io/api v0.22.1 k8s.io/apiextensions-apiserver v0.22.1 diff --git a/pkg/resmgr/changeset.go b/pkg/resmgr/changeset.go index 0004b13..6ae1f67 100644 --- a/pkg/resmgr/changeset.go +++ b/pkg/resmgr/changeset.go @@ -52,6 +52,8 @@ type ChangeSetEntry struct { Subject string // Action represents the action type taken by the reconciler for this object. Action string + // Diff contains the object diff. + Diff string } func (e ChangeSetEntry) String() string { diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index d006f36..eea5b49 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -20,6 +20,7 @@ package resmgr import ( "context" "fmt" + "github.com/google/go-cmp/cmp" "sort" "strings" "time" @@ -66,6 +67,28 @@ func NewResourceManager(kubeConfigPath, kubeContext, fieldOwner string) (*Resour }, nil } +// Diff performs a server-side apply dry-un and returns the fields that changed. +func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + if dryRunObject.GetResourceVersion() == "" { + return kc.changeSetEntry(dryRunObject, CreatedAction, ""), nil + } + + // do not apply objects that have not drifted to avoid bumping the resource version + if drift, diff := kc.hasDrifted(existingObject, dryRunObject); drift { + return kc.changeSetEntry(object, ConfiguredAction, diff), nil + } + + return kc.changeSetEntry(dryRunObject, UnchangedAction, ""), nil +} + // Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. // Drift detection is performed by comparing the server-side dry-run result with the existing object. // When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. @@ -88,8 +111,8 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr } // do not apply objects that have not drifted to avoid bumping the resource version - if !kc.hasDrifted(existingObject, dryRunObject) { - return kc.changeSetEntry(object, UnchangedAction), nil + if drift, _ := kc.hasDrifted(existingObject, dryRunObject); !drift { + return kc.changeSetEntry(object, UnchangedAction, ""), nil } appliedObject := object.DeepCopy() @@ -98,9 +121,9 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr } if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(appliedObject, CreatedAction), nil + return kc.changeSetEntry(appliedObject, CreatedAction, ""), nil } - return kc.changeSetEntry(appliedObject, ConfiguredAction), nil + return kc.changeSetEntry(appliedObject, ConfiguredAction, ""), nil } // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, @@ -127,15 +150,15 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) } - if kc.hasDrifted(existingObject, dryRunObject) { + if drift, _ := kc.hasDrifted(existingObject, dryRunObject); drift { toApply = append(toApply, object) if dryRunObject.GetResourceVersion() == "" { - changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) + changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction, "")) } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) + changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction, "")) } } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) + changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction, "")) } } @@ -208,7 +231,7 @@ func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructure if err != nil { return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) } else { - changeSet.Add(*kc.changeSetEntry(object, DeletedAction)) + changeSet.Add(*kc.changeSetEntry(object, DeletedAction, "")) } } } @@ -233,38 +256,39 @@ func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstr } // hasDrifted detects changes to metadata labels, metadata annotations and spec. -func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { +func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) (bool, string) { if dryRunObject.GetResourceVersion() == "" { - return true + return true, "" } if !apiequality.Semantic.DeepDerivative(dryRunObject.GetLabels(), existingObject.GetLabels()) { - return true + return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + } if !apiequality.Semantic.DeepDerivative(dryRunObject.GetAnnotations(), existingObject.GetAnnotations()) { - return true + return true, cmp.Diff(dryRunObject.Object, existingObject.Object) } if _, ok := existingObject.Object["spec"]; ok { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { - return true + return true, cmp.Diff(dryRunObject.Object, existingObject.Object) } } else if _, ok := existingObject.Object["webhooks"]; ok { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["webhooks"], existingObject.Object["webhooks"]) { - return true + return true, cmp.Diff(dryRunObject.Object, existingObject.Object) } } else { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { - return true + return true, cmp.Diff(dryRunObject.Object, existingObject.Object) } } - return false + return false, "" } -func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { - return &ChangeSetEntry{kc.fmt.Unstructured(object), string(action)} +func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action, diff string) *ChangeSetEntry { + return &ChangeSetEntry{kc.fmt.Unstructured(object), string(action), diff} } // Wait checks if the given set of objects has been fully reconciled. From 80d675d11642ad1ecc04f5fb94131b750e085b14 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 10:38:13 +0300 Subject: [PATCH 19/40] Remove managed fields from diff output Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 4 +++ cmd/kustomizer/diff.go | 17 +++++++--- cmd/kustomizer/main.go | 8 +++-- pkg/resmgr/changeset.go | 7 +++-- pkg/resmgr/manager.go | 63 ++++++++++++++++++++++---------------- 5 files changed, 63 insertions(+), 36 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index ded3c4d..e6c5536 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -42,6 +42,10 @@ jobs: run: | ./bin/kustomizer apply -i kustomizer-test -k testdata/kustomize/ --wait --prune kubectl -n kustomizer-test get tests 2>&1 | grep custom-resource-test1 + - name: Test diff --kustomize + run: | + echo "diff=test" >> testdata/kustomize/test.conf + ./bin/kustomizer diff -i kustomizer-test -k testdata/kustomize/ --prune | grep drifted - name: Test apply --filename run: | ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index 525e861..6aa3af6 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -19,13 +19,15 @@ package main import ( "context" "fmt" + "github.com/spf13/cobra" + "github.com/stefanprodan/kustomizer/pkg/resmgr" ) var diffCmd = &cobra.Command{ Use: "diff", - Short: "Diff Kubernetes manifests and Kustomize overlays using server-side dry-run.", + Short: "Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff.", RunE: runDiffCmd, } @@ -79,21 +81,26 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - if change.Diff != "" { - logger.Println(change.String()) + + if change.Action == string(resmgr.CreatedAction) { + logger.Println(`►`, change.Subject, "created") + } + + if change.Action == string(resmgr.ConfiguredAction) { + logger.Println(`►`, change.Subject, "drifted") logger.Println(change.Diff) } + } if diffArgs.inventoryName != "" { - staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, diffArgs.inventoryName, diffArgs.inventoryNamespace) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } for _, object := range staleObjects { - logger.Println(fmt.Sprintf("%s/%s/%s deleted", object.GetKind(), object.GetNamespace(), object.GetName())) + logger.Println(`►`, fmt.Sprintf("%s deleted", resourceFormatter.Unstructured(object))) } } return nil diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index b1b80e8..d3ef8f3 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "github.com/stefanprodan/kustomizer/pkg/resmgr" "os" "path/filepath" "time" @@ -45,9 +46,10 @@ type rootFlags struct { } var ( - rootArgs = rootFlags{} - logger = stderrLogger{stderr: os.Stderr} - inventoryMgr *inventory.InventoryManager + rootArgs = rootFlags{} + logger = stderrLogger{stderr: os.Stderr} + resourceFormatter = &resmgr.ResourceFormatter{} + inventoryMgr *inventory.InventoryManager ) func init() { diff --git a/pkg/resmgr/changeset.go b/pkg/resmgr/changeset.go index 6ae1f67..63f5259 100644 --- a/pkg/resmgr/changeset.go +++ b/pkg/resmgr/changeset.go @@ -34,15 +34,18 @@ type ChangeSet struct { Entries []ChangeSetEntry } +// NewChangeSet returns a ChangeSet will an empty slice of entries. func NewChangeSet() *ChangeSet { return &ChangeSet{Entries: []ChangeSetEntry{}} } +// Add appends the given entry to the end of the slice. func (c *ChangeSet) Add(e ChangeSetEntry) { c.Entries = append(c.Entries, e) } -func (c *ChangeSet) AddAll(e []ChangeSetEntry) { +// Append adds the given ChangeSet entries to end of the slice. +func (c *ChangeSet) Append(e []ChangeSetEntry) { c.Entries = append(c.Entries, e...) } @@ -52,7 +55,7 @@ type ChangeSetEntry struct { Subject string // Action represents the action type taken by the reconciler for this object. Action string - // Diff contains the object diff. + // Diff contains the YAML diff resulting from server-side apply dry-run. Diff string } diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index eea5b49..ce51187 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -20,11 +20,11 @@ package resmgr import ( "context" "fmt" - "github.com/google/go-cmp/cmp" "sort" "strings" "time" + "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,6 +37,7 @@ import ( "sigs.k8s.io/cli-utils/pkg/kstatus/status" "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" ) // ResourceManager reconciles Kubernetes resources onto the target cluster. @@ -78,15 +79,24 @@ func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstru } if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(dryRunObject, CreatedAction, ""), nil + return kc.changeSetEntry(dryRunObject, CreatedAction), nil } // do not apply objects that have not drifted to avoid bumping the resource version - if drift, diff := kc.hasDrifted(existingObject, dryRunObject); drift { - return kc.changeSetEntry(object, ConfiguredAction, diff), nil + if kc.hasDrifted(existingObject, dryRunObject) { + cse := kc.changeSetEntry(object, ConfiguredAction) + + unstructured.RemoveNestedField(dryRunObject.Object, "metadata", "managedFields") + unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") + + d, _ := yaml.Marshal(dryRunObject) + e, _ := yaml.Marshal(existingObject) + cse.Diff = cmp.Diff(string(d), string(e)) + + return cse, nil } - return kc.changeSetEntry(dryRunObject, UnchangedAction, ""), nil + return kc.changeSetEntry(dryRunObject, UnchangedAction), nil } // Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. @@ -111,8 +121,8 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr } // do not apply objects that have not drifted to avoid bumping the resource version - if drift, _ := kc.hasDrifted(existingObject, dryRunObject); !drift { - return kc.changeSetEntry(object, UnchangedAction, ""), nil + if !kc.hasDrifted(existingObject, dryRunObject) { + return kc.changeSetEntry(object, UnchangedAction), nil } appliedObject := object.DeepCopy() @@ -121,9 +131,10 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr } if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(appliedObject, CreatedAction, ""), nil + return kc.changeSetEntry(appliedObject, CreatedAction), nil } - return kc.changeSetEntry(appliedObject, ConfiguredAction, ""), nil + + return kc.changeSetEntry(appliedObject, ConfiguredAction), nil } // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, @@ -150,15 +161,15 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) } - if drift, _ := kc.hasDrifted(existingObject, dryRunObject); drift { + if kc.hasDrifted(existingObject, dryRunObject) { toApply = append(toApply, object) if dryRunObject.GetResourceVersion() == "" { - changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction, "")) + changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction, "")) + changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) } } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction, "")) + changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) } } @@ -198,7 +209,7 @@ func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstru if err != nil { return nil, err } - changeSet.AddAll(cs.Entries) + changeSet.Append(cs.Entries) if err := kc.Wait(stageOne, 2*time.Second, wait); err != nil { return nil, err @@ -209,7 +220,7 @@ func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstru if err != nil { return nil, err } - changeSet.AddAll(cs.Entries) + changeSet.Append(cs.Entries) return changeSet, nil } @@ -231,7 +242,7 @@ func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructure if err != nil { return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) } else { - changeSet.Add(*kc.changeSetEntry(object, DeletedAction, "")) + changeSet.Add(*kc.changeSetEntry(object, DeletedAction)) } } } @@ -256,39 +267,39 @@ func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstr } // hasDrifted detects changes to metadata labels, metadata annotations and spec. -func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) (bool, string) { +func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { if dryRunObject.GetResourceVersion() == "" { - return true, "" + return true } if !apiequality.Semantic.DeepDerivative(dryRunObject.GetLabels(), existingObject.GetLabels()) { - return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + return true } if !apiequality.Semantic.DeepDerivative(dryRunObject.GetAnnotations(), existingObject.GetAnnotations()) { - return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + return true } if _, ok := existingObject.Object["spec"]; ok { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { - return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + return true } } else if _, ok := existingObject.Object["webhooks"]; ok { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["webhooks"], existingObject.Object["webhooks"]) { - return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + return true } } else { if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { - return true, cmp.Diff(dryRunObject.Object, existingObject.Object) + return true } } - return false, "" + return false } -func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action, diff string) *ChangeSetEntry { - return &ChangeSetEntry{kc.fmt.Unstructured(object), string(action), diff} +func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { + return &ChangeSetEntry{Subject: kc.fmt.Unstructured(object), Action: string(action)} } // Wait checks if the given set of objects has been fully reconciled. From 73ce16c535d8b33aa32c83f33035e8078ca07960 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 11:39:41 +0300 Subject: [PATCH 20/40] Mask secrets in diff output Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 6 +++++- cmd/kustomizer/diff.go | 10 +++++----- pkg/resmgr/fmt.go | 20 ++++++++++++++++++++ pkg/resmgr/manager.go | 16 ++++++++++++++-- testdata/kustomize/kustomization.yaml | 7 ++++++- testdata/kustomize/secret.conf | 1 + 6 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 testdata/kustomize/secret.conf diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index e6c5536..254fbff 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -45,7 +45,11 @@ jobs: - name: Test diff --kustomize run: | echo "diff=test" >> testdata/kustomize/test.conf - ./bin/kustomizer diff -i kustomizer-test -k testdata/kustomize/ --prune | grep drifted + ./bin/kustomizer diff -i kustomizer-test -k testdata/kustomize/ --prune 2>&1 | grep drifted + - name: Test diff secret marks + run: | + echo "diff=test" >> testdata/kustomize/secret.conf + ./bin/kustomizer diff -i kustomizer-test -k testdata/kustomize/ 2>&1 | grep -F "secret.conf: '******'" - name: Test apply --filename run: | ./bin/kustomizer apply -i kustomizer-demo -f testdata/plain/ --wait --prune diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index 6aa3af6..c676f68 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -27,7 +27,7 @@ import ( var diffCmd = &cobra.Command{ Use: "diff", - Short: "Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff.", + Short: "Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff to stdout.", RunE: runDiffCmd, } @@ -83,12 +83,12 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { } if change.Action == string(resmgr.CreatedAction) { - logger.Println(`►`, change.Subject, "created") + fmt.Println(`►`, change.Subject, "created") } if change.Action == string(resmgr.ConfiguredAction) { - logger.Println(`►`, change.Subject, "drifted") - logger.Println(change.Diff) + fmt.Println(`►`, change.Subject, "drifted") + fmt.Println(change.Diff) } } @@ -100,7 +100,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { } for _, object := range staleObjects { - logger.Println(`►`, fmt.Sprintf("%s deleted", resourceFormatter.Unstructured(object))) + fmt.Println(`►`, fmt.Sprintf("%s deleted", resourceFormatter.Unstructured(object))) } } return nil diff --git a/pkg/resmgr/fmt.go b/pkg/resmgr/fmt.go index 8560685..45a954d 100644 --- a/pkg/resmgr/fmt.go +++ b/pkg/resmgr/fmt.go @@ -51,3 +51,23 @@ func (rf *ResourceFormatter) getSeparator() string { return rf.Separator } + +func (rf *ResourceFormatter) MaskSecret(object *unstructured.Unstructured, mask string) (*unstructured.Unstructured, error) { + data, found, err := unstructured.NestedMap(object.Object, "data") + if err != nil { + return nil, err + } + + if found { + for k, _ := range data { + data[k] = mask + } + + err = unstructured.SetNestedMap(object.Object, data, "data") + if err != nil { + return nil, err + } + } + + return object, err +} diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index ce51187..40a8f7b 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -82,16 +82,28 @@ func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstru return kc.changeSetEntry(dryRunObject, CreatedAction), nil } - // do not apply objects that have not drifted to avoid bumping the resource version if kc.hasDrifted(existingObject, dryRunObject) { cse := kc.changeSetEntry(object, ConfiguredAction) unstructured.RemoveNestedField(dryRunObject.Object, "metadata", "managedFields") unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") + if dryRunObject.GetKind() == "Secret" { + d, err := kc.fmt.MaskSecret(dryRunObject, "******") + if err != nil { + return nil, fmt.Errorf("masking secret data failed, error: %w", err) + } + dryRunObject = d + ex, err := kc.fmt.MaskSecret(existingObject, "*****") + if err != nil { + return nil, fmt.Errorf("masking secret data failed, error: %w", err) + } + existingObject = ex + } + d, _ := yaml.Marshal(dryRunObject) e, _ := yaml.Marshal(existingObject) - cse.Diff = cmp.Diff(string(d), string(e)) + cse.Diff = cmp.Diff(string(e), string(d)) return cse, nil } diff --git a/testdata/kustomize/kustomization.yaml b/testdata/kustomize/kustomization.yaml index 74565b7..1b414fb 100644 --- a/testdata/kustomize/kustomization.yaml +++ b/testdata/kustomize/kustomization.yaml @@ -11,4 +11,9 @@ configMapGenerator: - name: test-config files: - test.conf - +secretGenerator: + - name: test-secret + options: + disableNameSuffixHash: true + files: + - secret.conf diff --git a/testdata/kustomize/secret.conf b/testdata/kustomize/secret.conf new file mode 100644 index 0000000..82496c9 --- /dev/null +++ b/testdata/kustomize/secret.conf @@ -0,0 +1 @@ +token=demo2 \ No newline at end of file From 01094d2650b6aab38604185f88f360b21c173d84 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 12:51:13 +0300 Subject: [PATCH 21/40] Refactor resource manager Signed-off-by: Stefan Prodan --- pkg/resmgr/manager.go | 353 +---------------------------------- pkg/resmgr/manager_apply.go | 174 +++++++++++++++++ pkg/resmgr/manager_delete.go | 60 ++++++ pkg/resmgr/manager_diff.go | 105 +++++++++++ pkg/resmgr/manager_wait.go | 126 +++++++++++++ 5 files changed, 468 insertions(+), 350 deletions(-) create mode 100644 pkg/resmgr/manager_apply.go create mode 100644 pkg/resmgr/manager_delete.go create mode 100644 pkg/resmgr/manager_diff.go create mode 100644 pkg/resmgr/manager_wait.go diff --git a/pkg/resmgr/manager.go b/pkg/resmgr/manager.go index 40a8f7b..75f00ff 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/resmgr/manager.go @@ -18,26 +18,11 @@ limitations under the License. package resmgr import ( - "context" "fmt" - "sort" - "strings" - "time" - "github.com/google/go-cmp/cmp" - apiequality "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" - "sigs.k8s.io/cli-utils/pkg/kstatus/status" - "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" ) // ResourceManager reconciles Kubernetes resources onto the target cluster. @@ -68,343 +53,11 @@ func NewResourceManager(kubeConfigPath, kubeContext, fieldOwner string) (*Resour }, nil } -// Diff performs a server-side apply dry-un and returns the fields that changed. -func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { - existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - - dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) - } - - if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(dryRunObject, CreatedAction), nil - } - - if kc.hasDrifted(existingObject, dryRunObject) { - cse := kc.changeSetEntry(object, ConfiguredAction) - - unstructured.RemoveNestedField(dryRunObject.Object, "metadata", "managedFields") - unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") - - if dryRunObject.GetKind() == "Secret" { - d, err := kc.fmt.MaskSecret(dryRunObject, "******") - if err != nil { - return nil, fmt.Errorf("masking secret data failed, error: %w", err) - } - dryRunObject = d - ex, err := kc.fmt.MaskSecret(existingObject, "*****") - if err != nil { - return nil, fmt.Errorf("masking secret data failed, error: %w", err) - } - existingObject = ex - } - - d, _ := yaml.Marshal(dryRunObject) - e, _ := yaml.Marshal(existingObject) - cse.Diff = cmp.Diff(string(e), string(d)) - - return cse, nil - } - - return kc.changeSetEntry(dryRunObject, UnchangedAction), nil -} - -// Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. -// Drift detection is performed by comparing the server-side dry-run result with the existing object. -// When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. -func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { - existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - - dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { - if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { - return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) - } - return kc.Apply(ctx, object, force) - } - } - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) - } - - // do not apply objects that have not drifted to avoid bumping the resource version - if !kc.hasDrifted(existingObject, dryRunObject) { - return kc.changeSetEntry(object, UnchangedAction), nil - } - - appliedObject := object.DeepCopy() - if err := kc.apply(ctx, appliedObject); err != nil { - return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) - } - - if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(appliedObject, CreatedAction), nil - } - - return kc.changeSetEntry(appliedObject, ConfiguredAction), nil -} - -// ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, -// it applies the objects that are new or modified. -func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { - sort.Sort(ApplyOrder(objects)) - changeSet := NewChangeSet() - var toApply []*unstructured.Unstructured - for _, object := range objects { - existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - - dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { - if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { - return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) - } - return kc.ApplyAll(ctx, objects, force) - } - } - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) - } - - if kc.hasDrifted(existingObject, dryRunObject) { - toApply = append(toApply, object) - if dryRunObject.GetResourceVersion() == "" { - changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) - } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) - } - } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) - } - } - - for _, object := range toApply { - appliedObject := object.DeepCopy() - if err := kc.apply(ctx, appliedObject); err != nil { - return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) - } - } - - return changeSet, nil -} - -// ApplyAllStaged extracts the CRDs and Namespaces, applies them with ApplyAll, -// waits for CRDs and Namespaces to become ready, then is applies all the other objects. -// This function should be used when the given objects have a mix of custom resource definition and custom resources, -// or a mix of namespace definitions with namespaced objects. -func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstructured.Unstructured, force bool, wait time.Duration) (*ChangeSet, error) { - changeSet := NewChangeSet() - - // contains only CRDs and Namespaces - var stageOne []*unstructured.Unstructured - - // contains all objects except for CRDs and Namespaces - var stageTwo []*unstructured.Unstructured - - for _, u := range objects { - if IsClusterDefinition(u.GetKind()) { - stageOne = append(stageOne, u) - } else { - stageTwo = append(stageTwo, u) - } - } - - if len(stageOne) > 0 { - cs, err := kc.ApplyAll(ctx, stageOne, force) - if err != nil { - return nil, err - } - changeSet.Append(cs.Entries) - - if err := kc.Wait(stageOne, 2*time.Second, wait); err != nil { - return nil, err - } - } - - cs, err := kc.ApplyAll(ctx, stageTwo, force) - if err != nil { - return nil, err - } - changeSet.Append(cs.Entries) - - return changeSet, nil -} - -// DeleteAll deletes the given set of objects. -func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { - sort.Sort(sort.Reverse(ApplyOrder(objects))) - changeSet := NewChangeSet() - - for _, object := range objects { - existingObject := object.DeepCopy() - err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) - if err != nil { - if !apierrors.IsNotFound(err) { - return nil, fmt.Errorf("%s query failed, error: %w", kc.fmt.Unstructured(object), err) - } - } else { - err := kc.kubeClient.Delete(ctx, existingObject) - if err != nil { - return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) - } else { - changeSet.Add(*kc.changeSetEntry(object, DeletedAction)) - } - } - } - return changeSet, nil -} - -func (kc *ResourceManager) dryRunApply(ctx context.Context, object *unstructured.Unstructured) error { - opts := []client.PatchOption{ - client.DryRunAll, - client.ForceOwnership, - client.FieldOwner(kc.fieldOwner), - } - return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) -} - -func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { - opts := []client.PatchOption{ - client.ForceOwnership, - client.FieldOwner(kc.fieldOwner), - } - return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) -} - -// hasDrifted detects changes to metadata labels, metadata annotations and spec. -func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { - if dryRunObject.GetResourceVersion() == "" { - return true - } - - if !apiequality.Semantic.DeepDerivative(dryRunObject.GetLabels(), existingObject.GetLabels()) { - return true - - } - - if !apiequality.Semantic.DeepDerivative(dryRunObject.GetAnnotations(), existingObject.GetAnnotations()) { - return true - } - - if _, ok := existingObject.Object["spec"]; ok { - if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { - return true - } - } else if _, ok := existingObject.Object["webhooks"]; ok { - if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["webhooks"], existingObject.Object["webhooks"]) { - return true - } - } else { - if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { - return true - } - } - - return false +// KubeClient returns the underlying controller-runtime client. +func (kc *ResourceManager) KubeClient() client.Client { + return kc.kubeClient } func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { return &ChangeSetEntry{Subject: kc.fmt.Unstructured(object), Action: string(action)} } - -// Wait checks if the given set of objects has been fully reconciled. -func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { - objectsMeta := object.UnstructuredsToObjMetas(objects) - statusCollector := collector.NewResourceStatusCollector(objectsMeta) - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - opts := polling.Options{ - PollInterval: interval, - UseCache: true, - } - eventsChan := kc.kstatusPoller.Poll(ctx, objectsMeta, opts) - - lastStatus := make(map[object.ObjMetadata]*event.ResourceStatus) - - done := statusCollector.ListenWithObserver(eventsChan, collector.ObserverFunc( - func(statusCollector *collector.ResourceStatusCollector, e event.Event) { - var rss []*event.ResourceStatus - for _, rs := range statusCollector.ResourceStatuses { - if rs == nil { - continue - } - if rs.Error == nil { - lastStatus[rs.Identifier] = rs - } - rss = append(rss, rs) - } - desired := status.CurrentStatus - aggStatus := aggregator.AggregateStatus(rss, desired) - if aggStatus == desired { - cancel() - return - } - }), - ) - - <-done - - if statusCollector.Error != nil { - return statusCollector.Error - } - - if ctx.Err() == context.DeadlineExceeded { - var errors = []string{} - for id, rs := range statusCollector.ResourceStatuses { - if rs == nil { - errors = append(errors, fmt.Sprintf("can't determine status for %s", kc.fmt.ObjMetadata(id))) - continue - } - if lastStatus[id].Status != status.CurrentStatus { - var builder strings.Builder - builder.WriteString(fmt.Sprintf("%s status: '%s'", - kc.fmt.ObjMetadata(rs.Identifier), lastStatus[id].Status)) - if rs.Error != nil { - builder.WriteString(fmt.Sprintf(": %s", rs.Error)) - } - errors = append(errors, builder.String()) - } - } - return fmt.Errorf("timeout waiting for: [%s]", strings.Join(errors, ", ")) - } - - return nil -} - -// WaitForTermination waits for the given objects to be deleted from the cluster. -func (kc *ResourceManager) WaitForTermination(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - for _, object := range objects { - if err := wait.PollImmediate(interval, timeout, kc.isDeleted(ctx, object)); err != nil { - return err - } - } - return nil -} - -func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { - return func() (bool, error) { - obj := object.DeepCopy() - err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) - if apierrors.IsNotFound(err) { - return true, nil - } - return false, err - } -} - -// KubeClient returns the underlying controller-runtime client. -func (kc *ResourceManager) KubeClient() client.Client { - return kc.kubeClient -} diff --git a/pkg/resmgr/manager_apply.go b/pkg/resmgr/manager_apply.go new file mode 100644 index 0000000..ca504a8 --- /dev/null +++ b/pkg/resmgr/manager_apply.go @@ -0,0 +1,174 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. +// Drift detection is performed by comparing the server-side dry-run result with the existing object. +// When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. +func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) + } + return kc.Apply(ctx, object, force) + } + } + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + // do not apply objects that have not drifted to avoid bumping the resource version + if !kc.hasDrifted(existingObject, dryRunObject) { + return kc.changeSetEntry(object, UnchangedAction), nil + } + + appliedObject := object.DeepCopy() + if err := kc.apply(ctx, appliedObject); err != nil { + return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + } + + if dryRunObject.GetResourceVersion() == "" { + return kc.changeSetEntry(appliedObject, CreatedAction), nil + } + + return kc.changeSetEntry(appliedObject, ConfiguredAction), nil +} + +// ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, +// it applies the objects that are new or modified. +func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { + sort.Sort(ApplyOrder(objects)) + changeSet := NewChangeSet() + var toApply []*unstructured.Unstructured + for _, object := range objects { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) + } + return kc.ApplyAll(ctx, objects, force) + } + } + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + if kc.hasDrifted(existingObject, dryRunObject) { + toApply = append(toApply, object) + if dryRunObject.GetResourceVersion() == "" { + changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) + } else { + changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) + } + } else { + changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) + } + } + + for _, object := range toApply { + appliedObject := object.DeepCopy() + if err := kc.apply(ctx, appliedObject); err != nil { + return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + } + } + + return changeSet, nil +} + +// ApplyAllStaged extracts the CRDs and Namespaces, applies them with ApplyAll, +// waits for CRDs and Namespaces to become ready, then is applies all the other objects. +// This function should be used when the given objects have a mix of custom resource definition and custom resources, +// or a mix of namespace definitions with namespaced objects. +func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstructured.Unstructured, force bool, wait time.Duration) (*ChangeSet, error) { + changeSet := NewChangeSet() + + // contains only CRDs and Namespaces + var stageOne []*unstructured.Unstructured + + // contains all objects except for CRDs and Namespaces + var stageTwo []*unstructured.Unstructured + + for _, u := range objects { + if IsClusterDefinition(u.GetKind()) { + stageOne = append(stageOne, u) + } else { + stageTwo = append(stageTwo, u) + } + } + + if len(stageOne) > 0 { + cs, err := kc.ApplyAll(ctx, stageOne, force) + if err != nil { + return nil, err + } + changeSet.Append(cs.Entries) + + if err := kc.Wait(stageOne, 2*time.Second, wait); err != nil { + return nil, err + } + } + + cs, err := kc.ApplyAll(ctx, stageTwo, force) + if err != nil { + return nil, err + } + changeSet.Append(cs.Entries) + + return changeSet, nil +} + +func (kc *ResourceManager) dryRunApply(ctx context.Context, object *unstructured.Unstructured) error { + opts := []client.PatchOption{ + client.DryRunAll, + client.ForceOwnership, + client.FieldOwner(kc.fieldOwner), + } + return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) +} + +func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { + opts := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner(kc.fieldOwner), + } + return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) +} diff --git a/pkg/resmgr/manager_delete.go b/pkg/resmgr/manager_delete.go new file mode 100644 index 0000000..8ae4319 --- /dev/null +++ b/pkg/resmgr/manager_delete.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "context" + "fmt" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "sort" +) + +// Delete deletes the given object (not found errors are ignored). +func (kc *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { + existingObject := object.DeepCopy() + err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + if err != nil { + if !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("%s query failed, error: %w", kc.fmt.Unstructured(object), err) + } + } else { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) + } + } + + return kc.changeSetEntry(object, DeletedAction), nil +} + +// DeleteAll deletes the given set of objects (not found errors are ignored).. +func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { + sort.Sort(sort.Reverse(ApplyOrder(objects))) + changeSet := NewChangeSet() + + for _, object := range objects { + cse, err := kc.Delete(ctx, object) + if err != nil { + return nil, err + } + changeSet.Add(*cse) + } + + return changeSet, nil +} diff --git a/pkg/resmgr/manager_diff.go b/pkg/resmgr/manager_diff.go new file mode 100644 index 0000000..70da7af --- /dev/null +++ b/pkg/resmgr/manager_diff.go @@ -0,0 +1,105 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "context" + "fmt" + + "github.com/google/go-cmp/cmp" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" +) + +// Diff performs a server-side apply dry-un and returns the fields that changed. +// If the diff contains Kubernetes Secrets, the data values are masked. +func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { + existingObject := object.DeepCopy() + _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + + dryRunObject := object.DeepCopy() + if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + } + + if dryRunObject.GetResourceVersion() == "" { + return kc.changeSetEntry(dryRunObject, CreatedAction), nil + } + + if kc.hasDrifted(existingObject, dryRunObject) { + cse := kc.changeSetEntry(object, ConfiguredAction) + + unstructured.RemoveNestedField(dryRunObject.Object, "metadata", "managedFields") + unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") + + if dryRunObject.GetKind() == "Secret" { + d, err := kc.fmt.MaskSecret(dryRunObject, "******") + if err != nil { + return nil, fmt.Errorf("masking secret data failed, error: %w", err) + } + dryRunObject = d + ex, err := kc.fmt.MaskSecret(existingObject, "*****") + if err != nil { + return nil, fmt.Errorf("masking secret data failed, error: %w", err) + } + existingObject = ex + } + + d, _ := yaml.Marshal(dryRunObject) + e, _ := yaml.Marshal(existingObject) + cse.Diff = cmp.Diff(string(e), string(d)) + + return cse, nil + } + + return kc.changeSetEntry(dryRunObject, UnchangedAction), nil +} + +// hasDrifted detects changes to metadata labels, metadata annotations, spec and webhooks. +func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { + if dryRunObject.GetResourceVersion() == "" { + return true + } + + if !apiequality.Semantic.DeepDerivative(dryRunObject.GetLabels(), existingObject.GetLabels()) { + return true + + } + + if !apiequality.Semantic.DeepDerivative(dryRunObject.GetAnnotations(), existingObject.GetAnnotations()) { + return true + } + + if _, ok := existingObject.Object["spec"]; ok { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["spec"], existingObject.Object["spec"]) { + return true + } + } else if _, ok := existingObject.Object["webhooks"]; ok { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object["webhooks"], existingObject.Object["webhooks"]) { + return true + } + } else { + if !apiequality.Semantic.DeepDerivative(dryRunObject.Object, existingObject.Object) { + return true + } + } + + return false +} diff --git a/pkg/resmgr/manager_wait.go b/pkg/resmgr/manager_wait.go new file mode 100644 index 0000000..7881425 --- /dev/null +++ b/pkg/resmgr/manager_wait.go @@ -0,0 +1,126 @@ +/* +Copyright 2021 Stefan Prodan +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 resmgr + +import ( + "context" + "fmt" + "strings" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" + "sigs.k8s.io/cli-utils/pkg/kstatus/status" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Wait checks if the given set of objects has been fully reconciled. +func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { + objectsMeta := object.UnstructuredsToObjMetas(objects) + statusCollector := collector.NewResourceStatusCollector(objectsMeta) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + opts := polling.Options{ + PollInterval: interval, + UseCache: true, + } + eventsChan := kc.kstatusPoller.Poll(ctx, objectsMeta, opts) + + lastStatus := make(map[object.ObjMetadata]*event.ResourceStatus) + + done := statusCollector.ListenWithObserver(eventsChan, collector.ObserverFunc( + func(statusCollector *collector.ResourceStatusCollector, e event.Event) { + var rss []*event.ResourceStatus + for _, rs := range statusCollector.ResourceStatuses { + if rs == nil { + continue + } + if rs.Error == nil { + lastStatus[rs.Identifier] = rs + } + rss = append(rss, rs) + } + desired := status.CurrentStatus + aggStatus := aggregator.AggregateStatus(rss, desired) + if aggStatus == desired { + cancel() + return + } + }), + ) + + <-done + + if statusCollector.Error != nil { + return statusCollector.Error + } + + if ctx.Err() == context.DeadlineExceeded { + var errors = []string{} + for id, rs := range statusCollector.ResourceStatuses { + if rs == nil { + errors = append(errors, fmt.Sprintf("can't determine status for %s", kc.fmt.ObjMetadata(id))) + continue + } + if lastStatus[id].Status != status.CurrentStatus { + var builder strings.Builder + builder.WriteString(fmt.Sprintf("%s status: '%s'", + kc.fmt.ObjMetadata(rs.Identifier), lastStatus[id].Status)) + if rs.Error != nil { + builder.WriteString(fmt.Sprintf(": %s", rs.Error)) + } + errors = append(errors, builder.String()) + } + } + return fmt.Errorf("timeout waiting for: [%s]", strings.Join(errors, ", ")) + } + + return nil +} + +// WaitForTermination waits for the given objects to be deleted from the cluster. +func (kc *ResourceManager) WaitForTermination(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + for _, object := range objects { + if err := wait.PollImmediate(interval, timeout, kc.isDeleted(ctx, object)); err != nil { + return err + } + } + return nil +} + +func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { + return func() (bool, error) { + obj := object.DeepCopy() + err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } +} From cdf7e25bbff02933093d5f70f1537a16166176de Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 14:26:46 +0300 Subject: [PATCH 22/40] Remove sensitive data from secrets validation errors Signed-off-by: Stefan Prodan --- cmd/kustomizer/diff.go | 10 +++++++++- pkg/resmgr/manager_apply.go | 4 ++-- pkg/resmgr/manager_diff.go | 18 +++++++++++++++++- testdata/invalid/test.yaml | 22 ++++++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 testdata/invalid/test.yaml diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index c676f68..9405864 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "os" "github.com/spf13/cobra" @@ -76,10 +77,13 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() + invalid := false for _, object := range objects { change, err := resMgr.Diff(ctx, object) if err != nil { - return err + logger.Println(`✗`, err) + invalid = true + continue } if change.Action == string(resmgr.CreatedAction) { @@ -103,5 +107,9 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { fmt.Println(`►`, fmt.Sprintf("%s deleted", resourceFormatter.Unstructured(object))) } } + + if invalid { + os.Exit(1) + } return nil } diff --git a/pkg/resmgr/manager_apply.go b/pkg/resmgr/manager_apply.go index ca504a8..39c1bf3 100644 --- a/pkg/resmgr/manager_apply.go +++ b/pkg/resmgr/manager_apply.go @@ -48,7 +48,7 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr return kc.Apply(ctx, object, force) } } - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + return nil, kc.validationError(dryRunObject, err) } // do not apply objects that have not drifted to avoid bumping the resource version @@ -89,7 +89,7 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured return kc.ApplyAll(ctx, objects, force) } } - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + return nil, kc.validationError(dryRunObject, err) } if kc.hasDrifted(existingObject, dryRunObject) { diff --git a/pkg/resmgr/manager_diff.go b/pkg/resmgr/manager_diff.go index 70da7af..d7b2c67 100644 --- a/pkg/resmgr/manager_diff.go +++ b/pkg/resmgr/manager_diff.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -36,7 +37,7 @@ func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstru dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - return nil, fmt.Errorf("%s apply dry-run failed, error: %w", kc.fmt.Unstructured(dryRunObject), err) + return nil, kc.validationError(dryRunObject, err) } if dryRunObject.GetResourceVersion() == "" { @@ -103,3 +104,18 @@ func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured return false } + +// validationError formats the given error and hides sensitive data +// if the error was caused by an invalid Kubernetes secrets. +func (kc *ResourceManager) validationError(object *unstructured.Unstructured, err error) error { + if apierrors.IsNotFound(err) { + return fmt.Errorf("%s namespace not specified, error: %w", kc.fmt.Unstructured(object), err) + } + + if object.GetKind() == "Secret" { + return fmt.Errorf("%s is invalid, error: data values must be of type string", kc.fmt.Unstructured(object)) + } + + return fmt.Errorf("%s is invalid, error: %w", kc.fmt.Unstructured(object), err) + +} diff --git a/testdata/invalid/test.yaml b/testdata/invalid/test.yaml new file mode 100644 index 0000000..ac49ab5 --- /dev/null +++ b/testdata/invalid/test.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Secret +metadata: + name: invalid1 +stringData: + token: 0 +--- +apiVersion: v1 +kind: Secret +metadata: + name: invalid2 + namespace: default +stringData: + token: 0 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: invalid3 + namespace: default +data: + token: 0 From 636be1b8a1e061671d295c63eb9cb79e352fd09c Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 14:48:05 +0300 Subject: [PATCH 23/40] Improve delete cmd output Signed-off-by: Stefan Prodan --- cmd/kustomizer/delete.go | 24 ++++++++++++++++++------ cmd/kustomizer/main.go | 2 +- pkg/resmgr/manager_diff.go | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 41fca14..68aef07 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -19,6 +19,8 @@ package main import ( "context" "fmt" + "os" + "sort" "time" "github.com/spf13/cobra" @@ -27,7 +29,7 @@ import ( var deleteCmd = &cobra.Command{ Use: "delete", - Short: "Delete the Kubernetes objects in the inventory.", + Short: "Delete the Kubernetes objects in the inventory including the inventory configmap.", RunE: deleteCmdRun, } @@ -63,6 +65,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() + logger.Println("retrieving inventory...") inv, err := inventoryMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) if err != nil { return err @@ -73,14 +76,23 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return err } - changeSet, err := resMgr.DeleteAll(ctx, objects) - if err != nil { - return err - } - for _, change := range changeSet.Entries { + logger.Println(fmt.Sprintf("deleting %v manifest(s)...", len(objects))) + hasErrors := false + sort.Sort(sort.Reverse(resmgr.ApplyOrder(objects))) + for _, object := range objects { + change, err := resMgr.Delete(ctx, object) + if err != nil { + logger.Println(`✗`, err) + hasErrors = true + continue + } logger.Println(change.String()) } + if hasErrors { + os.Exit(1) + } + err = inventoryMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) if err != nil { return err diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index d3ef8f3..a5981d6 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -73,7 +73,7 @@ func main() { } if err := rootCmd.Execute(); err != nil { - logger.Println(err) + logger.Println(`✗`, err) os.Exit(1) } } diff --git a/pkg/resmgr/manager_diff.go b/pkg/resmgr/manager_diff.go index d7b2c67..13115a7 100644 --- a/pkg/resmgr/manager_diff.go +++ b/pkg/resmgr/manager_diff.go @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/yaml" ) -// Diff performs a server-side apply dry-un and returns the fields that changed. +// Diff performs a server-side apply dry-un and returns the fields that changed in YAML format. // If the diff contains Kubernetes Secrets, the data values are masked. func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() From 7d6d2eb3422824d3e24ffa6dee4b25362889c4be Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 19:57:50 +0300 Subject: [PATCH 24/40] Detect immutable secrets Signed-off-by: Stefan Prodan --- pkg/resmgr/manager_apply.go | 28 ++++++++++++---------------- pkg/resmgr/manager_diff.go | 7 ++++++- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/pkg/resmgr/manager_apply.go b/pkg/resmgr/manager_apply.go index 39c1bf3..468eab0 100644 --- a/pkg/resmgr/manager_apply.go +++ b/pkg/resmgr/manager_apply.go @@ -24,8 +24,6 @@ import ( "strings" "time" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -39,15 +37,14 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { - if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { - return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) - } - return kc.Apply(ctx, object, force) + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) } + return kc.Apply(ctx, object, force) } + return nil, kc.validationError(dryRunObject, err) } @@ -80,15 +77,14 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - if _, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid); ok { - if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { - return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) - } - return kc.ApplyAll(ctx, objects, force) + if force && strings.Contains(err.Error(), "immutable") { + if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", + kc.fmt.Unstructured(dryRunObject), err) } + return kc.ApplyAll(ctx, objects, force) } + return nil, kc.validationError(dryRunObject, err) } diff --git a/pkg/resmgr/manager_diff.go b/pkg/resmgr/manager_diff.go index 13115a7..7e34962 100644 --- a/pkg/resmgr/manager_diff.go +++ b/pkg/resmgr/manager_diff.go @@ -20,6 +20,7 @@ package resmgr import ( "context" "fmt" + "strings" "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -113,7 +114,11 @@ func (kc *ResourceManager) validationError(object *unstructured.Unstructured, er } if object.GetKind() == "Secret" { - return fmt.Errorf("%s is invalid, error: data values must be of type string", kc.fmt.Unstructured(object)) + msg := "data values must be of type string" + if strings.Contains(err.Error(), "immutable") { + msg = "secret is is immutable" + } + return fmt.Errorf("%s is invalid, error: %s", kc.fmt.Unstructured(object), msg) } return fmt.Errorf("%s is invalid, error: %w", kc.fmt.Unstructured(object), err) From f3dee1cb9ca464dc5dc3377e38f12e073c76ac24 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 19:59:03 +0300 Subject: [PATCH 25/40] Setup controller-runtime envtest Signed-off-by: Stefan Prodan --- Makefile | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index bbb52bd..b4aa867 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION?=$(shell grep 'VERSION' cmd/kustomizer/main.go | awk '{ print $$4 }' | tr -d '"') +# Kustomizer test, build, release makefile all: test build @@ -11,9 +11,6 @@ fmt: vet: go vet ./... -test: tidy fmt vet - go test ./... -coverprofile cover.out - build: CGO_ENABLED=0 go build -o ./bin/kustomizer ./cmd/kustomizer @@ -26,18 +23,43 @@ install-dev: install-plugin: CGO_ENABLED=0 go build -o /usr/local/bin/kubectl-kustomizer ./cmd/kustomizer -release: - git tag "v$(VERSION)" - git push origin "v$(VERSION)" +ENVTEST_ASSETS_DIR=$(shell pwd)/testbin +ENVTEST_AKUBERNETES_VERSION=latest +install-envtest: setup-envtest + $(SETUP_ENVTEST) use $(ENVTEST_AKUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) + +KUBEBUILDER_ASSETS?="$(shell $(SETUP_ENVTEST) use -i $(ENVTEST_AKUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" +test: tidy fmt vet install-envtest + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -coverprofile cover.out + +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +setup-envtest: +ifeq (, $(shell which setup-envtest)) + @{ \ + set -e ;\ + SETUP_ENVTEST_TMP_DIR=$$(mktemp -d) ;\ + cd $$SETUP_ENVTEST_TMP_DIR ;\ + go mod init tmp ;\ + go get sigs.k8s.io/controller-runtime/tools/setup-envtest@latest ;\ + rm -rf $$SETUP_ENVTEST_TMP_DIR ;\ + } +SETUP_ENVTEST=$(GOBIN)/setup-envtest +else +SETUP_ENVTEST=$(shell which setup-envtest) +endif .PHONY: release-docs release-docs: - git checkout master && git pull; \ + git checkout main && git pull; \ README=$$(cat README.md); \ git checkout gh-pages && git pull; \ echo "$$README" > README.md; \ - git add -A; \ + git add README.md; \ git commit -m "update docs"; \ git push origin gh-pages; \ - git checkout master - + git checkout main From 612faddc9694de0f91b55182ece22eb6dd5c9074 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Thu, 2 Sep 2021 19:59:33 +0300 Subject: [PATCH 26/40] Add resource manager apply tests Signed-off-by: Stefan Prodan --- .gitignore | 3 +- go.sum | 3 + pkg/resmgr/main_test.go | 125 ++++++++++++++++++ pkg/resmgr/manager_apply_test.go | 211 +++++++++++++++++++++++++++++++ pkg/resmgr/manager_diff.go | 2 +- pkg/resmgr/testdata/test1.yaml | 53 ++++++++ 6 files changed, 395 insertions(+), 2 deletions(-) create mode 100644 pkg/resmgr/main_test.go create mode 100644 pkg/resmgr/manager_apply_test.go create mode 100644 pkg/resmgr/testdata/test1.yaml diff --git a/.gitignore b/.gitignore index b9d8154..d21d574 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ -bin/ \ No newline at end of file +bin/ +testbin/ diff --git a/go.sum b/go.sum index 8059d68..87b482c 100644 --- a/go.sum +++ b/go.sum @@ -258,6 +258,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -310,6 +311,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -1043,6 +1045,7 @@ k8s.io/client-go v0.22.1/go.mod h1:BquC5A4UOo4qVDUtoc04/+Nxp1MeHcVc1HJm1KmG8kk= k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= +k8s.io/component-base v0.22.1 h1:SFqIXsEN3v3Kkr1bS6rstrs1wd45StJqbtgbQ4nRQdo= k8s.io/component-base v0.22.1/go.mod h1:0D+Bl8rrnsPN9v0dyYvkqFfBeAd4u7n77ze+p8CMiPo= k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= diff --git a/pkg/resmgr/main_test.go b/pkg/resmgr/main_test.go new file mode 100644 index 0000000..26125c8 --- /dev/null +++ b/pkg/resmgr/main_test.go @@ -0,0 +1,125 @@ +package resmgr + +import ( + "fmt" + "io" + "os" + "strings" + "sync/atomic" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +var manager *ResourceManager + +func TestMain(m *testing.M) { + testEnv := &envtest.Environment{} + + cfg, err := testEnv.Start() + if err != nil { + panic(err) + } + + kubeClient, err := client.NewWithWatch(cfg, client.Options{Scheme: newScheme()}) + if err != nil { + panic(err) + } + + restMapper, err := apiutil.NewDynamicRESTMapper(cfg) + if err != nil { + panic(err) + } + + c, err := client.New(cfg, client.Options{Mapper: restMapper}) + if err != nil { + panic(err) + } + + poller := polling.NewStatusPoller(c, restMapper) + + manager = &ResourceManager{ + kubeClient: kubeClient, + kstatusPoller: poller, + fmt: &ResourceFormatter{}, + fieldOwner: "resource-manager", + } + + code := m.Run() + + testEnv.Stop() + + os.Exit(code) +} + +func readManifest(manifest, namespace string) ([]*unstructured.Unstructured, error) { + data, err := os.ReadFile(manifest) + if err != nil { + return nil, err + } + yml := fmt.Sprintf(string(data), namespace) + + reader := yamlutil.NewYAMLOrJSONDecoder(strings.NewReader(yml), 2048) + objects := make([]*unstructured.Unstructured, 0) + for { + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + if err == io.EOF { + err = nil + break + } + return objects, err + } + + if obj.IsList() { + err = obj.EachListItem(func(item apiruntime.Object) error { + obj := item.(*unstructured.Unstructured) + objects = append(objects, obj) + return nil + }) + if err != nil { + return objects, err + } + continue + } + + objects = append(objects, obj) + } + + return objects, nil + +} + +func setNamespace(objects []*unstructured.Unstructured, namespace string) { + for _, object := range objects { + object.SetNamespace(namespace) + } + + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "", + Kind: "Namespace", + Version: "v1", + }) + u.SetName(namespace) + objects = append(objects, u) +} + +var nextNameId int64 + +func generateName(prefix string) string { + id := atomic.AddInt64(&nextNameId, 1) + return fmt.Sprintf("%s-%d", prefix, id) +} + +func removeObject(s []*unstructured.Unstructured, index int) []*unstructured.Unstructured { + return append(s[:index], s[index+1:]...) +} diff --git a/pkg/resmgr/manager_apply_test.go b/pkg/resmgr/manager_apply_test.go new file mode 100644 index 0000000..3ba0588 --- /dev/null +++ b/pkg/resmgr/manager_apply_test.go @@ -0,0 +1,211 @@ +package resmgr + +import ( + "context" + "encoding/base64" + "fmt" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestApply(t *testing.T) { + timeout := 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + objects, err := readManifest("testdata/test1.yaml", generateName("ns")) + if err != nil { + t.Fatal(err) + } + + // create objects + createdChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) + } + + // expected created order + sort.Sort(ApplyOrder(objects)) + var expected []string + for _, object := range objects { + expected = append(expected, manager.fmt.Unstructured(object)) + } + + // verify the change set contains only created actions + var output []string + for _, entry := range createdChangeSet.Entries { + if diff := cmp.Diff(entry.Action, string(CreatedAction)); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + output = append(output, entry.Subject) + } + + // verify the change set contains all objects in the right order + if diff := cmp.Diff(expected, output); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + // no-op apply + unchangedChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) + } + + // verify the change set contains only unchanged actions + for _, entry := range unchangedChangeSet.Entries { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + output = append(output, entry.Subject) + } + + // extract configmap + var configMap *unstructured.Unstructured + for _, object := range objects { + if object.GetKind() == "ConfigMap" { + configMap = object + break + } + } + configMapName := manager.fmt.Unstructured(configMap) + + // update a value in the configmap + err = unstructured.SetNestedField(configMap.Object, "val", "data", "key") + if err != nil { + t.Fatal(err) + } + + // apply changes + configuredChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) + } + + // verify the change set contains the configured action only for the configmap + for _, entry := range configuredChangeSet.Entries { + if entry.Subject == configMapName { + if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + } + + // get the configmap from cluster + configMapClone := configMap.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) + if err != nil { + t.Fatal(err) + } + + // get data value from the in-cluster configmap + val, _, err := unstructured.NestedFieldCopy(configMapClone.Object, "data", "key") + if err != nil { + t.Fatal(err) + } + + // verify the configmap was updated in cluster with the right data value + if diff := cmp.Diff(val, "val"); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + // delete the configmap + deletedChangeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{configMap}) + for _, entry := range deletedChangeSet.Entries { + if diff := cmp.Diff(string(DeletedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + + // reapply objects + changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) + } + + // verify the configmap was recreated + for _, entry := range changeSet.Entries { + if entry.Subject == configMapName { + if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + } + + // extract secret + var secret *unstructured.Unstructured + for _, object := range objects { + if object.GetKind() == "Secret" { + secret = object + break + } + } + secretName := manager.fmt.Unstructured(secret) + + // update a value in the secret + err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key") + if err != nil { + t.Fatal(err) + } + + // apply and expect to fail + changeSet, err = manager.ApplyAllStaged(ctx, objects, false, timeout) + if err == nil { + t.Fatal("Expected error got none") + } + + // verify that the error message does not contain sensitive information + expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", manager.fmt.Unstructured(secret)) + if diff := cmp.Diff(expectedErr, err.Error()); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + // force apply + changeSet, err = manager.ApplyAllStaged(ctx, objects, true, timeout) + if err != nil { + t.Fatal(err) + } + + // verify the secret was recreated + for _, entry := range changeSet.Entries { + if entry.Subject == secretName { + if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + } + + // get the secret from cluster + secretClone := secret.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) + if err != nil { + t.Fatal(err) + } + + // get data value from the in-cluster secret + val, _, err = unstructured.NestedFieldCopy(secretClone.Object, "data", "key") + if err != nil { + t.Fatal(err) + } + + // verify the secret was updated in cluster with the right data value + if diff := cmp.Diff(val, base64.StdEncoding.EncodeToString([]byte("val-secret"))); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } +} diff --git a/pkg/resmgr/manager_diff.go b/pkg/resmgr/manager_diff.go index 7e34962..763e57b 100644 --- a/pkg/resmgr/manager_diff.go +++ b/pkg/resmgr/manager_diff.go @@ -116,7 +116,7 @@ func (kc *ResourceManager) validationError(object *unstructured.Unstructured, er if object.GetKind() == "Secret" { msg := "data values must be of type string" if strings.Contains(err.Error(), "immutable") { - msg = "secret is is immutable" + msg = "secret is immutable" } return fmt.Errorf("%s is invalid, error: %s", kc.fmt.Unstructured(object), msg) } diff --git a/pkg/resmgr/testdata/test1.yaml b/pkg/resmgr/testdata/test1.yaml new file mode 100644 index 0000000..f9c60b2 --- /dev/null +++ b/pkg/resmgr/testdata/test1.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: "%[1]s" + namespace: "%[1]s" +--- +apiVersion: v1 +kind: Secret +metadata: + name: "%[1]s" + namespace: "%[1]s" +immutable: true +stringData: + key: "private-key" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "%[1]s" + namespace: "%[1]s" +data: + key: "public-key" +--- +apiVersion: v1 +kind: Namespace +metadata: + name: "%[1]s" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: "%[1]s" +rules: + - apiGroups: + - apps + resources: ["*"] + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: "%[1]s" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: "%[1]s" +subjects: + - kind: ServiceAccount + name: "%[1]s" + namespace: "%[1]s" From faf34647ea4345b5e8ea654aa22e627cc1f676e8 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 09:09:30 +0300 Subject: [PATCH 27/40] Refactor resource manager apply tests Signed-off-by: Stefan Prodan --- Makefile | 2 +- pkg/resmgr/manager_apply_test.go | 317 ++++++++++++++++--------------- 2 files changed, 168 insertions(+), 151 deletions(-) diff --git a/Makefile b/Makefile index b4aa867..6d191f2 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ install-envtest: setup-envtest KUBEBUILDER_ASSETS?="$(shell $(SETUP_ENVTEST) use -i $(ENVTEST_AKUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" test: tidy fmt vet install-envtest - KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -coverprofile cover.out + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -v -parallel 4 -coverprofile cover.out ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin diff --git a/pkg/resmgr/manager_apply_test.go b/pkg/resmgr/manager_apply_test.go index 3ba0588..c0c11de 100644 --- a/pkg/resmgr/manager_apply_test.go +++ b/pkg/resmgr/manager_apply_test.go @@ -18,194 +18,211 @@ func TestApply(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - objects, err := readManifest("testdata/test1.yaml", generateName("ns")) - if err != nil { - t.Fatal(err) - } + var configMap *unstructured.Unstructured + var configMapName string + + var secret *unstructured.Unstructured + var secretName string - // create objects - createdChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + objects, err := readManifest("testdata/test1.yaml", generateName("ns")) if err != nil { t.Fatal(err) } - // expected created order - sort.Sort(ApplyOrder(objects)) - var expected []string - for _, object := range objects { - expected = append(expected, manager.fmt.Unstructured(object)) - } - - // verify the change set contains only created actions - var output []string - for _, entry := range createdChangeSet.Entries { - if diff := cmp.Diff(entry.Action, string(CreatedAction)); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + t.Run("creates objects in order", func(t *testing.T) { + // create objects + changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) } - output = append(output, entry.Subject) - } - // verify the change set contains all objects in the right order - if diff := cmp.Diff(expected, output); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } + // expected created order + sort.Sort(ApplyOrder(objects)) + var expected []string + for _, object := range objects { + expected = append(expected, manager.fmt.Unstructured(object)) + } - // no-op apply - unchangedChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) - if err != nil { - t.Fatal(err) - } + // verify the change set contains only created actions + var output []string + for _, entry := range changeSet.Entries { + if diff := cmp.Diff(entry.Action, string(CreatedAction)); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + output = append(output, entry.Subject) + } - // verify the change set contains only unchanged actions - for _, entry := range unchangedChangeSet.Entries { - if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + // verify the change set contains all objects in the right order + if diff := cmp.Diff(expected, output); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } - output = append(output, entry.Subject) - } + }) - // extract configmap - var configMap *unstructured.Unstructured - for _, object := range objects { - if object.GetKind() == "ConfigMap" { - configMap = object - break + t.Run("does not apply unchanged objects", func(t *testing.T) { + // no-op apply + unchangedChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) } - } - configMapName := manager.fmt.Unstructured(configMap) - - // update a value in the configmap - err = unstructured.SetNestedField(configMap.Object, "val", "data", "key") - if err != nil { - t.Fatal(err) - } - - // apply changes - configuredChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) - if err != nil { - t.Fatal(err) - } - // verify the change set contains the configured action only for the configmap - for _, entry := range configuredChangeSet.Entries { - if entry.Subject == configMapName { - if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } else { + // verify the change set contains only unchanged actions + var output []string + for _, entry := range unchangedChangeSet.Entries { if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } + output = append(output, entry.Subject) } - } + }) + + t.Run("applies only changed objects", func(t *testing.T) { + // extract configmap + for _, object := range objects { + if object.GetKind() == "ConfigMap" { + configMap = object + break + } + } + configMapName = manager.fmt.Unstructured(configMap) - // get the configmap from cluster - configMapClone := configMap.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) - if err != nil { - t.Fatal(err) - } + // update a value in the configmap + err = unstructured.SetNestedField(configMap.Object, "val", "data", "key") + if err != nil { + t.Fatal(err) + } - // get data value from the in-cluster configmap - val, _, err := unstructured.NestedFieldCopy(configMapClone.Object, "data", "key") - if err != nil { - t.Fatal(err) - } + // apply changes + configuredChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) + } - // verify the configmap was updated in cluster with the right data value - if diff := cmp.Diff(val, "val"); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } + // verify the change set contains the configured action only for the configmap + for _, entry := range configuredChangeSet.Entries { + if entry.Subject == configMapName { + if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + } - // delete the configmap - deletedChangeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{configMap}) - for _, entry := range deletedChangeSet.Entries { - if diff := cmp.Diff(string(DeletedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + // get the configmap from cluster + configMapClone := configMap.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) + if err != nil { + t.Fatal(err) } - } - // reapply objects - changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) - if err != nil { - t.Fatal(err) - } + // get data value from the in-cluster configmap + val, _, err := unstructured.NestedFieldCopy(configMapClone.Object, "data", "key") + if err != nil { + t.Fatal(err) + } - // verify the configmap was recreated - for _, entry := range changeSet.Entries { - if entry.Subject == configMapName { - if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } else { - if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + // verify the configmap was updated in cluster with the right data value + if diff := cmp.Diff(val, "val"); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + }) + + t.Run("recreates deleted objects", func(t *testing.T) { + // delete the configmap + deletedChangeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{configMap}) + for _, entry := range deletedChangeSet.Entries { + if diff := cmp.Diff(string(DeletedAction), entry.Action); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } } - } - // extract secret - var secret *unstructured.Unstructured - for _, object := range objects { - if object.GetKind() == "Secret" { - secret = object - break + // reapply objects + changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err != nil { + t.Fatal(err) } - } - secretName := manager.fmt.Unstructured(secret) - // update a value in the secret - err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key") - if err != nil { - t.Fatal(err) - } + // verify the configmap was recreated + for _, entry := range changeSet.Entries { + if entry.Subject == configMapName { + if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } + } + }) + + t.Run("fails to apply immutable secrets", func(t *testing.T) { + // extract secret + for _, object := range objects { + if object.GetKind() == "Secret" { + secret = object + break + } + } + secretName = manager.fmt.Unstructured(secret) - // apply and expect to fail - changeSet, err = manager.ApplyAllStaged(ctx, objects, false, timeout) - if err == nil { - t.Fatal("Expected error got none") - } + // update a value in the secret + err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key") + if err != nil { + t.Fatal(err) + } - // verify that the error message does not contain sensitive information - expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", manager.fmt.Unstructured(secret)) - if diff := cmp.Diff(expectedErr, err.Error()); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } + // apply and expect to fail + _, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + if err == nil { + t.Fatal("Expected error got none") + } - // force apply - changeSet, err = manager.ApplyAllStaged(ctx, objects, true, timeout) - if err != nil { - t.Fatal(err) - } + // verify that the error message does not contain sensitive information + expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", manager.fmt.Unstructured(secret)) + if diff := cmp.Diff(expectedErr, err.Error()); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + }) - // verify the secret was recreated - for _, entry := range changeSet.Entries { - if entry.Subject == secretName { - if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } else { - if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + t.Run("force applies immutable secrets", func(t *testing.T) { + // force apply + changeSet, err := manager.ApplyAllStaged(ctx, objects, true, timeout) + if err != nil { + t.Fatal(err) + } + + // verify the secret was recreated + for _, entry := range changeSet.Entries { + if entry.Subject == secretName { + if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } } } - } - // get the secret from cluster - secretClone := secret.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) - if err != nil { - t.Fatal(err) - } + // get the secret from cluster + secretClone := secret.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) + if err != nil { + t.Fatal(err) + } - // get data value from the in-cluster secret - val, _, err = unstructured.NestedFieldCopy(secretClone.Object, "data", "key") - if err != nil { - t.Fatal(err) - } + // get data value from the in-cluster secret + val, _, err := unstructured.NestedFieldCopy(secretClone.Object, "data", "key") + if err != nil { + t.Fatal(err) + } - // verify the secret was updated in cluster with the right data value - if diff := cmp.Diff(val, base64.StdEncoding.EncodeToString([]byte("val-secret"))); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } + // verify the secret was updated in cluster with the right data value + if diff := cmp.Diff(val, base64.StdEncoding.EncodeToString([]byte("val-secret"))); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + }) } From 09515443e38e4d31404efc71d24b17d9b34387ba Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 09:58:17 +0300 Subject: [PATCH 28/40] Add diff tests Signed-off-by: Stefan Prodan --- pkg/resmgr/main_test.go | 10 ++++ pkg/resmgr/manager_apply_test.go | 42 ++++--------- pkg/resmgr/manager_diff_test.go | 100 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 31 deletions(-) create mode 100644 pkg/resmgr/manager_diff_test.go diff --git a/pkg/resmgr/main_test.go b/pkg/resmgr/main_test.go index 26125c8..156d73c 100644 --- a/pkg/resmgr/main_test.go +++ b/pkg/resmgr/main_test.go @@ -19,6 +19,7 @@ import ( ) var manager *ResourceManager +var resFmt = &ResourceFormatter{} func TestMain(m *testing.M) { testEnv := &envtest.Environment{} @@ -120,6 +121,15 @@ func generateName(prefix string) string { return fmt.Sprintf("%s-%d", prefix, id) } +func getObjectFrom(objects []*unstructured.Unstructured, kind, name string) (string, *unstructured.Unstructured) { + for _, object := range objects { + if object.GetKind() == kind && object.GetName() == name { + return resFmt.Unstructured(object), object + } + } + return "", nil +} + func removeObject(s []*unstructured.Unstructured, index int) []*unstructured.Unstructured { return append(s[:index], s[index+1:]...) } diff --git a/pkg/resmgr/manager_apply_test.go b/pkg/resmgr/manager_apply_test.go index c0c11de..335e6ac 100644 --- a/pkg/resmgr/manager_apply_test.go +++ b/pkg/resmgr/manager_apply_test.go @@ -18,17 +18,15 @@ func TestApply(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - var configMap *unstructured.Unstructured - var configMapName string - - var secret *unstructured.Unstructured - var secretName string - - objects, err := readManifest("testdata/test1.yaml", generateName("ns")) + id := generateName("apply") + objects, err := readManifest("testdata/test1.yaml", id) if err != nil { t.Fatal(err) } + configMapName, configMap := getObjectFrom(objects, "ConfigMap", id) + secretName, secret := getObjectFrom(objects, "Secret", id) + t.Run("creates objects in order", func(t *testing.T) { // create objects changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) @@ -60,14 +58,14 @@ func TestApply(t *testing.T) { t.Run("does not apply unchanged objects", func(t *testing.T) { // no-op apply - unchangedChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) if err != nil { t.Fatal(err) } // verify the change set contains only unchanged actions var output []string - for _, entry := range unchangedChangeSet.Entries { + for _, entry := range changeSet.Entries { if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } @@ -76,15 +74,6 @@ func TestApply(t *testing.T) { }) t.Run("applies only changed objects", func(t *testing.T) { - // extract configmap - for _, object := range objects { - if object.GetKind() == "ConfigMap" { - configMap = object - break - } - } - configMapName = manager.fmt.Unstructured(configMap) - // update a value in the configmap err = unstructured.SetNestedField(configMap.Object, "val", "data", "key") if err != nil { @@ -92,13 +81,13 @@ func TestApply(t *testing.T) { } // apply changes - configuredChangeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) + changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) if err != nil { t.Fatal(err) } // verify the change set contains the configured action only for the configmap - for _, entry := range configuredChangeSet.Entries { + for _, entry := range changeSet.Entries { if entry.Subject == configMapName { if diff := cmp.Diff(string(ConfiguredAction), entry.Action); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) @@ -158,16 +147,7 @@ func TestApply(t *testing.T) { } }) - t.Run("fails to apply immutable secrets", func(t *testing.T) { - // extract secret - for _, object := range objects { - if object.GetKind() == "Secret" { - secret = object - break - } - } - secretName = manager.fmt.Unstructured(secret) - + t.Run("fails to apply immutable secret", func(t *testing.T) { // update a value in the secret err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key") if err != nil { @@ -187,7 +167,7 @@ func TestApply(t *testing.T) { } }) - t.Run("force applies immutable secrets", func(t *testing.T) { + t.Run("force applies immutable secret", func(t *testing.T) { // force apply changeSet, err := manager.ApplyAllStaged(ctx, objects, true, timeout) if err != nil { diff --git a/pkg/resmgr/manager_diff_test.go b/pkg/resmgr/manager_diff_test.go new file mode 100644 index 0000000..f31f949 --- /dev/null +++ b/pkg/resmgr/manager_diff_test.go @@ -0,0 +1,100 @@ +package resmgr + +import ( + "context" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestDiff(t *testing.T) { + timeout := 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + id := generateName("diff") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + t.Fatal(err) + } + + configMapName, configMap := getObjectFrom(objects, "ConfigMap", id) + secretName, secret := getObjectFrom(objects, "Secret", id) + + if err := unstructured.SetNestedField(secret.Object, false, "immutable"); err != nil { + t.Fatal(err) + } + if _, err = manager.ApplyAllStaged(ctx, objects, false, timeout); err != nil { + t.Fatal(err) + } + + t.Run("generates empty diff for unchanged object", func(t *testing.T) { + changeSetEntry, err := manager.Diff(ctx, configMap) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(configMapName, changeSetEntry.Subject); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(string(UnchangedAction), changeSetEntry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + }) + + t.Run("generates diff for changed object", func(t *testing.T) { + newVal := "diff-test" + err = unstructured.SetNestedField(configMap.Object, newVal, "data", "key") + if err != nil { + t.Fatal(err) + } + + changeSetEntry, err := manager.Diff(ctx, configMap) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(string(ConfiguredAction), changeSetEntry.Action); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + if !strings.Contains(changeSetEntry.Diff, newVal) { + t.Errorf("Mismatch from expected value, want %s", newVal) + } + }) + + t.Run("masks secret values", func(t *testing.T) { + newVal := "diff-test" + err = unstructured.SetNestedField(secret.Object, newVal, "stringData", "key") + if err != nil { + t.Fatal(err) + } + + newKey := "key.new" + err = unstructured.SetNestedField(secret.Object, newVal, "stringData", newKey) + if err != nil { + t.Fatal(err) + } + + changeSetEntry, err := manager.Diff(ctx, secret) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(secretName, changeSetEntry.Subject); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + if !strings.Contains(changeSetEntry.Diff, newKey) { + t.Errorf("Mismatch from expected value, got %s", changeSetEntry.Diff) + } + + if strings.Contains(changeSetEntry.Diff, newVal) { + t.Errorf("Mismatch from expected value, got %s", changeSetEntry.Diff) + } + }) +} From e5583cf4e58f6a64f9988d72b20652064b53bd8e Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 10:25:04 +0300 Subject: [PATCH 29/40] Add delete tests Signed-off-by: Stefan Prodan --- pkg/resmgr/manager_delete_test.go | 90 +++++++++++++++++++++++++++++++ pkg/resmgr/manager_diff_test.go | 2 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 pkg/resmgr/manager_delete_test.go diff --git a/pkg/resmgr/manager_delete_test.go b/pkg/resmgr/manager_delete_test.go new file mode 100644 index 0000000..7390486 --- /dev/null +++ b/pkg/resmgr/manager_delete_test.go @@ -0,0 +1,90 @@ +package resmgr + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestDelete(t *testing.T) { + timeout := 10 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + id := generateName("delete") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + t.Fatal(err) + } + + _, role := getObjectFrom(objects, "ClusterRole", id) + _, binding := getObjectFrom(objects, "ClusterRoleBinding", id) + _, configMap := getObjectFrom(objects, "ConfigMap", id) + _, secret := getObjectFrom(objects, "Secret", id) + + if _, err = manager.ApplyAllStaged(ctx, objects, false, timeout); err != nil { + t.Fatal(err) + } + + t.Run("deletes objects in order", func(t *testing.T) { + changeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{binding, configMap}) + if err != nil { + t.Fatal(err) + } + + // expected deleted order + var expected []string + for _, object := range []*unstructured.Unstructured{configMap, binding} { + expected = append(expected, manager.fmt.Unstructured(object)) + } + + // verify the change set contains only created actions + var output []string + for _, entry := range changeSet.Entries { + if diff := cmp.Diff(entry.Action, string(DeletedAction)); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + output = append(output, entry.Subject) + } + + // verify the change set contains all objects in the right order + if diff := cmp.Diff(expected, output); diff != "" { + t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + } + + configMapClone := configMap.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) + if !apierrors.IsNotFound(err) { + t.Fatal(err) + } + + bindingClone := binding.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(bindingClone), bindingClone) + if !apierrors.IsNotFound(err) { + t.Fatal(err) + } + }) + + t.Run("waits for objects termination", func(t *testing.T) { + set := []*unstructured.Unstructured{role, secret} + _, err := manager.DeleteAll(ctx, set) + if err != nil { + t.Fatal(err) + } + + if err := manager.WaitForTermination(set, time.Second, 5*time.Second); err != nil { + t.Fatal(err) + } + + secretClone := secret.DeepCopy() + err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) + if !apierrors.IsNotFound(err) { + t.Fatal(err) + } + }) +} diff --git a/pkg/resmgr/manager_diff_test.go b/pkg/resmgr/manager_diff_test.go index f31f949..b08c6a2 100644 --- a/pkg/resmgr/manager_diff_test.go +++ b/pkg/resmgr/manager_diff_test.go @@ -2,12 +2,12 @@ package resmgr import ( "context" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "strings" "testing" "time" "github.com/google/go-cmp/cmp" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func TestDiff(t *testing.T) { From cbfd86c7ed2c3f28613c5864a8714ab9f566bb2b Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 10:38:55 +0300 Subject: [PATCH 30/40] Run tests with race detection Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 2 +- Makefile | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 254fbff..cec7ba5 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -29,7 +29,7 @@ jobs: version: v0.11.1 image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 - name: Run test - run: make test + run: make test-race - name: Check if working tree is dirty run: | if [[ $(git diff --stat) != '' ]]; then diff --git a/Makefile b/Makefile index 6d191f2..85bd97b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Kustomizer test, build, release makefile +# Kustomizer test, build, install makefile all: test build @@ -32,6 +32,9 @@ KUBEBUILDER_ASSETS?="$(shell $(SETUP_ENVTEST) use -i $(ENVTEST_AKUBERNETES_VERSI test: tidy fmt vet install-envtest KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -v -parallel 4 -coverprofile cover.out +test-race: tidy fmt vet install-envtest + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -v -race -parallel 4 -coverprofile cover.out + ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else From 9d50a5c09dfdfd186450664d4e19b04c5ecb7a81 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 17:10:05 +0300 Subject: [PATCH 31/40] Refactor package structure Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 21 +++- cmd/kustomizer/build.go | 12 +- {pkg/resmgr => cmd/kustomizer}/client.go | 2 +- cmd/kustomizer/delete.go | 31 +++-- cmd/kustomizer/diff.go | 28 +++-- cmd/kustomizer/main.go | 11 +- pkg/inventory/doc.go | 4 +- pkg/inventory/inventory.go | 85 ++++++++++---- pkg/inventory/manager.go | 98 +--------------- pkg/inventory/sort.go | 60 ---------- pkg/{resmgr => manager}/changeset.go | 2 +- pkg/{resmgr => manager}/doc.go | 4 +- pkg/{resmgr => manager}/main_test.go | 60 +++------- pkg/{resmgr => manager}/manager.go | 47 ++++---- pkg/{resmgr => manager}/manager_apply.go | 31 ++--- pkg/{resmgr => manager}/manager_apply_test.go | 15 +-- pkg/{resmgr => manager}/manager_delete.go | 13 ++- .../manager_delete_test.go | 11 +- pkg/{resmgr => manager}/manager_diff.go | 15 +-- pkg/{resmgr => manager}/manager_diff_test.go | 2 +- pkg/{resmgr => manager}/manager_wait.go | 11 +- pkg/manager/owner.go | 30 +++++ pkg/{resmgr => manager}/testdata/test1.yaml | 0 pkg/objectutil/doc.go | 19 +++ pkg/{resmgr => objectutil}/fmt.go | 29 ++--- pkg/objectutil/io.go | 109 ++++++++++++++++++ pkg/{resmgr => objectutil}/sort.go | 4 +- 27 files changed, 396 insertions(+), 358 deletions(-) rename {pkg/resmgr => cmd/kustomizer}/client.go (99%) delete mode 100644 pkg/inventory/sort.go rename pkg/{resmgr => manager}/changeset.go (99%) rename pkg/{resmgr => manager}/doc.go (92%) rename pkg/{resmgr => manager}/main_test.go (62%) rename pkg/{resmgr => manager}/manager.go (61%) rename pkg/{resmgr => manager}/manager_apply.go (83%) rename pkg/{resmgr => manager}/manager_apply_test.go (92%) rename pkg/{resmgr => manager}/manager_delete.go (76%) rename pkg/{resmgr => manager}/manager_delete_test.go (84%) rename pkg/{resmgr => manager}/manager_diff.go (87%) rename pkg/{resmgr => manager}/manager_diff_test.go (99%) rename pkg/{resmgr => manager}/manager_wait.go (92%) create mode 100644 pkg/manager/owner.go rename pkg/{resmgr => manager}/testdata/test1.yaml (100%) create mode 100644 pkg/objectutil/doc.go rename pkg/{resmgr => objectutil}/fmt.go (63%) create mode 100644 pkg/objectutil/io.go rename pkg/{resmgr => objectutil}/sort.go (94%) diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 584a42b..52cbc70 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -22,7 +22,8 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/resmgr" + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/manager" ) var applyCmd = &cobra.Command{ @@ -77,17 +78,27 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return err } - newInventory, err := inventoryMgr.Record(objects) - if err != nil { + newInventory := inventory.NewInventory(applyArgs.inventoryName, applyArgs.inventoryNamespace) + if err := newInventory.AddObjects(objects); err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } logger.Println(fmt.Sprintf("applying %v manifest(s)...", len(objects))) - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { - return err + return fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("status poller init failed: %w", err) } + resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ + Field: PROJECT, + Group: PROJECT + ".dev", + }) + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/kustomizer/build.go b/cmd/kustomizer/build.go index 0b211fd..29de4f2 100644 --- a/cmd/kustomizer/build.go +++ b/cmd/kustomizer/build.go @@ -32,7 +32,7 @@ import ( kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filesys" - "github.com/stefanprodan/kustomizer/pkg/resmgr" + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) var buildCmd = &cobra.Command{ @@ -72,13 +72,13 @@ func runBuildCmd(cmd *cobra.Command, args []string) error { switch buildArgs.output { case "yaml": - yml, err := inventoryMgr.ToYAML(objects) + yml, err := objectutil.ObjectsToYAML(objects) if err != nil { return err } fmt.Println(yml) case "json": - json, err := inventoryMgr.ToJSON(objects) + json, err := objectutil.ObjectsToJSON(objects) if err != nil { return err } @@ -98,7 +98,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U return nil, err } - objs, err := inventoryMgr.ReadAll(bytes.NewReader(data)) + objs, err := objectutil.ReadObjects(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("%s: %w", kustomizePath, err) } @@ -116,7 +116,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U return nil, err } - objs, err := inventoryMgr.ReadAll(bufio.NewReader(ms)) + objs, err := objectutil.ReadObjects(bufio.NewReader(ms)) ms.Close() if err != nil { return nil, fmt.Errorf("%s: %w", manifest, err) @@ -125,7 +125,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U } } - sort.Sort(resmgr.ApplyOrder(objects)) + sort.Sort(objectutil.ApplyOrder(objects)) return objects, nil } diff --git a/pkg/resmgr/client.go b/cmd/kustomizer/client.go similarity index 99% rename from pkg/resmgr/client.go rename to cmd/kustomizer/client.go index cd3eb1f..37ed33e 100644 --- a/pkg/resmgr/client.go +++ b/cmd/kustomizer/client.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package main import ( "fmt" diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index 68aef07..e2f0554 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -24,7 +24,9 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/resmgr" + + "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) var deleteCmd = &cobra.Command{ @@ -57,28 +59,39 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("--inventory-namespace is required") } - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) - if err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() logger.Println("retrieving inventory...") - inv, err := inventoryMgr.Retrieve(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("status poller init failed: %w", err) + } + + resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ + Field: PROJECT, + Group: PROJECT + ".dev", + }) + + inv, err := inventoryMgr.Retrieve(ctx, kubeClient, deleteArgs.inventoryName, deleteArgs.inventoryNamespace) if err != nil { return err } - objects, err := inv.List() + objects, err := inv.ListObjects() if err != nil { return err } logger.Println(fmt.Sprintf("deleting %v manifest(s)...", len(objects))) hasErrors := false - sort.Sort(sort.Reverse(resmgr.ApplyOrder(objects))) + sort.Sort(sort.Reverse(objectutil.ApplyOrder(objects))) for _, object := range objects { change, err := resMgr.Delete(ctx, object) if err != nil { diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index 9405864..6e0c80c 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -23,7 +23,9 @@ import ( "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/resmgr" + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) var diffCmd = &cobra.Command{ @@ -64,16 +66,26 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { return err } - newInventory, err := inventoryMgr.Record(objects) - if err != nil { + newInventory := inventory.NewInventory(applyArgs.inventoryName, applyArgs.inventoryNamespace) + if err := newInventory.AddObjects(objects); err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } - resMgr, err := resmgr.NewResourceManager(rootArgs.kubeconfig, rootArgs.kubecontext, PROJECT) + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { - return err + return fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("status poller init failed: %w", err) } + resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ + Field: PROJECT, + Group: PROJECT + ".dev", + }) + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -86,11 +98,11 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { continue } - if change.Action == string(resmgr.CreatedAction) { + if change.Action == string(manager.CreatedAction) { fmt.Println(`►`, change.Subject, "created") } - if change.Action == string(resmgr.ConfiguredAction) { + if change.Action == string(manager.ConfiguredAction) { fmt.Println(`►`, change.Subject, "drifted") fmt.Println(change.Diff) } @@ -104,7 +116,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { } for _, object := range staleObjects { - fmt.Println(`►`, fmt.Sprintf("%s deleted", resourceFormatter.Unstructured(object))) + fmt.Println(`►`, fmt.Sprintf("%s deleted", objectutil.FmtUnstructured(object))) } } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index a5981d6..6bb3883 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -17,14 +17,14 @@ limitations under the License. package main import ( - "github.com/stefanprodan/kustomizer/pkg/resmgr" "os" "path/filepath" "time" "github.com/spf13/cobra" - "github.com/stefanprodan/kustomizer/pkg/inventory" _ "k8s.io/client-go/plugin/pkg/client/auth" + + "github.com/stefanprodan/kustomizer/pkg/inventory" ) var VERSION = "1.0.0-dev.0" @@ -46,10 +46,9 @@ type rootFlags struct { } var ( - rootArgs = rootFlags{} - logger = stderrLogger{stderr: os.Stderr} - resourceFormatter = &resmgr.ResourceFormatter{} - inventoryMgr *inventory.InventoryManager + rootArgs = rootFlags{} + logger = stderrLogger{stderr: os.Stderr} + inventoryMgr *inventory.InventoryManager ) func init() { diff --git a/pkg/inventory/doc.go b/pkg/inventory/doc.go index 42e4e9a..c83ee0a 100644 --- a/pkg/inventory/doc.go +++ b/pkg/inventory/doc.go @@ -18,7 +18,7 @@ limitations under the License. // Package inventory contains utilities for keeping a record of Kubernetes objects applied on a cluster. // // The InventoryManager performs the following actions: -// - decodes raw manifests (YAML & JSON) into Kubernetes objects -// - records the objects metadata and stores the inventory in a Kubernetes ConfigMap +// - records the Kubernetes objects metadata in a compacted format +// - stores the inventory in a Kubernetes ConfigMap // - determines which objects are subject to garbage collection package inventory diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index f75b59b..9006b07 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -23,60 +23,95 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/object" + + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) -// Inventory is a record of objects that are applied on a cluster. +// Inventory is a record of objects that are applied on a cluster stored as a configmap. type Inventory struct { - Entries map[string]string `json:"entries"` + // Name of the inventory configmap. + Name string + + // Namespace of the inventory configmap. + Namespace string + + // Entries of Kubernetes objects metadata. + Entries []Entry `json:"entries"` } -func NewInventory() *Inventory { - return &Inventory{Entries: map[string]string{}} +// Entry is a record of a Kubernetes object metadata. +type Entry struct { + ObjectID string `json:"id"` + ObjectVersion string `json:"ver"` +} + +func NewInventory(name, namespace string) *Inventory { + return &Inventory{ + Name: name, + Namespace: namespace, + Entries: []Entry{}, + } } -// Add adds the given objects to the inventory. -func (inv *Inventory) Add(objects []*unstructured.Unstructured) error { +// AddObjects extracts the metadata from the given objects and adds it to the inventory. +func (inv *Inventory) AddObjects(objects []*unstructured.Unstructured) error { + sort.Sort(objectutil.ApplyOrder(objects)) for _, om := range objects { - objMeta := object.UnstructuredToObjMeta(om) + objMetadata := object.UnstructuredToObjMeta(om) gv, err := schema.ParseGroupVersion(om.GetAPIVersion()) if err != nil { return err } - inv.Entries[objMeta.String()] = gv.Version + + inv.Entries = append(inv.Entries, Entry{ + ObjectID: objMetadata.String(), + ObjectVersion: gv.Version, + }) } return nil } -// List returns the inventory entries as unstructured.Unstructured objects. -func (inv *Inventory) List() ([]*unstructured.Unstructured, error) { - objects := make([]*unstructured.Unstructured, 0) - list, err := inv.ListMeta() - if err != nil { - return nil, err +// VersionOf returns the API version of the given object if found in this inventory. +func (inv *Inventory) VersionOf(objMetadata object.ObjMetadata) string { + for _, entry := range inv.Entries { + if entry.ObjectID == objMetadata.String() { + return entry.ObjectVersion + } } + return "" +} + +// ListObjects returns the inventory entries as unstructured.Unstructured objects. +func (inv *Inventory) ListObjects() ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + + for _, entry := range inv.Entries { + objMetadata, err := object.ParseObjMetadata(entry.ObjectID) + if err != nil { + return nil, err + } - for _, metadata := range list { u := &unstructured.Unstructured{} u.SetGroupVersionKind(schema.GroupVersionKind{ - Group: metadata.GroupKind.Group, - Kind: metadata.GroupKind.Kind, - Version: inv.Entries[metadata.String()], + Group: objMetadata.GroupKind.Group, + Kind: objMetadata.GroupKind.Kind, + Version: entry.ObjectVersion, }) - u.SetName(metadata.Name) - u.SetNamespace(metadata.Namespace) + u.SetName(objMetadata.Name) + u.SetNamespace(objMetadata.Namespace) objects = append(objects, u) } - sort.Sort(InventoryOrder(objects)) + sort.Sort(objectutil.ApplyOrder(objects)) return objects, nil } // ListMeta returns the inventory entries as object.ObjMetadata objects. func (inv *Inventory) ListMeta() ([]object.ObjMetadata, error) { var metas []object.ObjMetadata - for e, _ := range inv.Entries { - m, err := object.ParseObjMetadata(e) + for _, e := range inv.Entries { + m, err := object.ParseObjMetadata(e.ObjectID) if err != nil { return metas, err } @@ -109,13 +144,13 @@ func (inv *Inventory) Diff(target *Inventory) ([]*unstructured.Unstructured, err u.SetGroupVersionKind(schema.GroupVersionKind{ Group: metadata.GroupKind.Group, Kind: metadata.GroupKind.Kind, - Version: inv.Entries[metadata.String()], + Version: inv.VersionOf(metadata), }) u.SetName(metadata.Name) u.SetNamespace(metadata.Namespace) objects = append(objects, u) } - sort.Sort(InventoryOrder(objects)) + sort.Sort(objectutil.ApplyOrder(objects)) return objects, nil } diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go index f6617bf..5d34fd5 100644 --- a/pkg/inventory/manager.go +++ b/pkg/inventory/manager.go @@ -21,18 +21,13 @@ import ( "context" "encoding/json" "fmt" - "io" - "strings" "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - apiruntime "k8s.io/apimachinery/pkg/runtime" - yamlutil "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" ) // InventoryManager records the Kubernetes objects that are applied on the cluster. @@ -56,17 +51,6 @@ func NewInventoryManager(fieldOwner, group string) (*InventoryManager, error) { }, nil } -// Record creates an Inventory of the given objects. -func (im *InventoryManager) Record(objects []*unstructured.Unstructured) (*Inventory, error) { - inventory := NewInventory() - - if err := inventory.Add(objects); err != nil { - return nil, err - } - - return inventory, nil -} - // Store applies the Inventory object on the server. func (im *InventoryManager) Store(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) error { data, err := json.Marshal(inv.Entries) @@ -103,7 +87,7 @@ func (im *InventoryManager) Retrieve(ctx context.Context, kubeClient client.Clie return nil, fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) } - var entries map[string]string + var entries []Entry err = json.Unmarshal([]byte(cm.Data["inventory"]), &entries) if err != nil { return nil, err @@ -143,86 +127,6 @@ func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client return nil } -// Read decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. -func (im *InventoryManager) Read(r io.Reader) (*unstructured.Unstructured, error) { - reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) - obj := &unstructured.Unstructured{} - err := reader.Decode(obj) - if err != nil { - return nil, err - } - - return obj, nil -} - -// ReadAll decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects. -func (im *InventoryManager) ReadAll(r io.Reader) ([]*unstructured.Unstructured, error) { - reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) - objects := make([]*unstructured.Unstructured, 0) - - for { - obj := &unstructured.Unstructured{} - err := reader.Decode(obj) - if err != nil { - if err == io.EOF { - err = nil - break - } - return objects, err - } - - if obj.IsList() { - err = obj.EachListItem(func(item apiruntime.Object) error { - obj := item.(*unstructured.Unstructured) - objects = append(objects, obj) - return nil - }) - if err != nil { - return objects, err - } - continue - } - - objects = append(objects, obj) - } - - return objects, nil -} - -// ToYAML encodes the given Kubernetes API objects to a YAML multi-doc. -func (im *InventoryManager) ToYAML(objects []*unstructured.Unstructured) (string, error) { - var builder strings.Builder - for _, obj := range objects { - data, err := yaml.Marshal(obj) - if err != nil { - return "", err - } - builder.Write(data) - builder.WriteString("---\n") - } - return builder.String(), nil -} - -// ToJSON encodes the given Kubernetes API objects to a YAML multi-doc. -func (im *InventoryManager) ToJSON(objects []*unstructured.Unstructured) (string, error) { - list := struct { - ApiVersion string `json:"apiVersion,omitempty"` - Kind string `json:"kind,omitempty"` - Items []*unstructured.Unstructured `json:"items,omitempty"` - }{ - ApiVersion: "v1", - Kind: "ListMeta", - Items: objects, - } - - data, err := json.MarshalIndent(list, "", " ") - if err != nil { - return "", err - } - - return string(data), nil -} - func (im *InventoryManager) newConfigMap(name, namespace string) *corev1.ConfigMap { return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ diff --git a/pkg/inventory/sort.go b/pkg/inventory/sort.go deleted file mode 100644 index 73f725b..0000000 --- a/pkg/inventory/sort.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2021 Stefan Prodan -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 inventory - -import ( - "strings" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// InventoryOrder implements the Sort interface for unstructured objects. -type InventoryOrder []*unstructured.Unstructured - -func (objects InventoryOrder) Len() int { - return len(objects) -} - -func (objects InventoryOrder) Swap(i, j int) { - objects[i], objects[j] = objects[j], objects[i] -} - -func (objects InventoryOrder) Less(i, j int) bool { - ki := objects[i].GetKind() - ni := objects[i].GetName() - kj := objects[j].GetKind() - nj := objects[j].GetName() - ranki, rankj := rankOfKind(ki), rankOfKind(kj) - if ranki == rankj { - return ni < nj - } - return ranki < rankj -} - -// rankOfKind returns an int denoting the position of the given kind -// in the partial ordering of Kubernetes resources. -func rankOfKind(kind string) int { - switch strings.ToLower(kind) { - case "customresourcedefinition": - return 0 - case "namespace": - return 1 - default: - return 2 - } -} diff --git a/pkg/resmgr/changeset.go b/pkg/manager/changeset.go similarity index 99% rename from pkg/resmgr/changeset.go rename to pkg/manager/changeset.go index 63f5259..e13b9ed 100644 --- a/pkg/resmgr/changeset.go +++ b/pkg/manager/changeset.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import "fmt" diff --git a/pkg/resmgr/doc.go b/pkg/manager/doc.go similarity index 92% rename from pkg/resmgr/doc.go rename to pkg/manager/doc.go index 44ab388..8fdbac3 100644 --- a/pkg/resmgr/doc.go +++ b/pkg/manager/doc.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package resmgr contains utilities for managing Kubernetes resources. +// Package manager contains utilities for managing Kubernetes resources. // // The ResourceManager performs the following actions: // - orders the Kubernetes objects for apply (CRDs, Namespaces, ClusterRoles first) @@ -25,4 +25,4 @@ limitations under the License. // - waits for the objects to be fully reconciled by looking up their readiness status // - deletes objects that are subject to garbage collection // - waits for the deleted objects to be terminated -package resmgr +package manager diff --git a/pkg/resmgr/main_test.go b/pkg/manager/main_test.go similarity index 62% rename from pkg/resmgr/main_test.go rename to pkg/manager/main_test.go index 156d73c..f840e35 100644 --- a/pkg/resmgr/main_test.go +++ b/pkg/manager/main_test.go @@ -1,17 +1,15 @@ -package resmgr +package manager import ( "fmt" - "io" "os" "strings" "sync/atomic" "testing" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - yamlutil "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" @@ -19,7 +17,6 @@ import ( ) var manager *ResourceManager -var resFmt = &ResourceFormatter{} func TestMain(m *testing.M) { testEnv := &envtest.Environment{} @@ -29,28 +26,27 @@ func TestMain(m *testing.M) { panic(err) } - kubeClient, err := client.NewWithWatch(cfg, client.Options{Scheme: newScheme()}) - if err != nil { - panic(err) - } - restMapper, err := apiutil.NewDynamicRESTMapper(cfg) if err != nil { panic(err) } - c, err := client.New(cfg, client.Options{Mapper: restMapper}) + kubeClient, err := client.New(cfg, client.Options{ + Mapper: restMapper, + }) if err != nil { panic(err) } - poller := polling.NewStatusPoller(c, restMapper) + poller := polling.NewStatusPoller(kubeClient, restMapper) manager = &ResourceManager{ - kubeClient: kubeClient, - kstatusPoller: poller, - fmt: &ResourceFormatter{}, - fieldOwner: "resource-manager", + client: kubeClient, + poller: poller, + owner: Owner{ + Field: "resource-manager", + Group: "resource-manager.io", + }, } code := m.Run() @@ -67,36 +63,12 @@ func readManifest(manifest, namespace string) ([]*unstructured.Unstructured, err } yml := fmt.Sprintf(string(data), namespace) - reader := yamlutil.NewYAMLOrJSONDecoder(strings.NewReader(yml), 2048) - objects := make([]*unstructured.Unstructured, 0) - for { - obj := &unstructured.Unstructured{} - err := reader.Decode(obj) - if err != nil { - if err == io.EOF { - err = nil - break - } - return objects, err - } - - if obj.IsList() { - err = obj.EachListItem(func(item apiruntime.Object) error { - obj := item.(*unstructured.Unstructured) - objects = append(objects, obj) - return nil - }) - if err != nil { - return objects, err - } - continue - } - - objects = append(objects, obj) + objects, err := objectutil.ReadObjects(strings.NewReader(yml)) + if err != nil { + return nil, err } return objects, nil - } func setNamespace(objects []*unstructured.Unstructured, namespace string) { @@ -124,7 +96,7 @@ func generateName(prefix string) string { func getObjectFrom(objects []*unstructured.Unstructured, kind, name string) (string, *unstructured.Unstructured) { for _, object := range objects { if object.GetKind() == kind && object.GetName() == name { - return resFmt.Unstructured(object), object + return objectutil.FmtUnstructured(object), object } } return "", nil diff --git a/pkg/resmgr/manager.go b/pkg/manager/manager.go similarity index 61% rename from pkg/resmgr/manager.go rename to pkg/manager/manager.go index 75f00ff..f8f8191 100644 --- a/pkg/resmgr/manager.go +++ b/pkg/manager/manager.go @@ -15,49 +15,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import ( - "fmt" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) // ResourceManager reconciles Kubernetes resources onto the target cluster. type ResourceManager struct { - kubeClient client.WithWatch - kstatusPoller *polling.StatusPoller - fmt *ResourceFormatter - fieldOwner string + client client.Client + poller *polling.StatusPoller + owner Owner } // NewResourceManager creates a ResourceManager for the given Kubernetes client config and context. -func NewResourceManager(kubeConfigPath, kubeContext, fieldOwner string) (*ResourceManager, error) { - kubeClient, err := newKubeClient(kubeConfigPath, kubeContext) - if err != nil { - return nil, fmt.Errorf("client init failed: %w", err) - } - - statusPoller, err := newKubeStatusPoller(kubeConfigPath, kubeContext) - if err != nil { - return nil, fmt.Errorf("status poller init failed: %w", err) - } - +func NewResourceManager(client client.Client, poller *polling.StatusPoller, owner Owner) *ResourceManager { return &ResourceManager{ - kubeClient: kubeClient, - kstatusPoller: statusPoller, - fmt: &ResourceFormatter{}, - fieldOwner: fieldOwner, - }, nil + client: client, + poller: poller, + owner: owner, + } } // KubeClient returns the underlying controller-runtime client. func (kc *ResourceManager) KubeClient() client.Client { - return kc.kubeClient + return kc.client } func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { - return &ChangeSetEntry{Subject: kc.fmt.Unstructured(object), Action: string(action)} + return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} } + +//func (kc *ResourceManager) SetOwnerLabels(objects []*unstructured.Unstructured, name, namespace string) { +// for _, object := range objects { +// object.SetLabels(map[string]string{ +// kc.fieldOwner + "/name": name, +// kc.fieldOwner + "/namespace": namespace, +// }) +// } +//} diff --git a/pkg/resmgr/manager_apply.go b/pkg/manager/manager_apply.go similarity index 83% rename from pkg/resmgr/manager_apply.go rename to pkg/manager/manager_apply.go index 468eab0..7b7c174 100644 --- a/pkg/resmgr/manager_apply.go +++ b/pkg/manager/manager_apply.go @@ -15,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "sort" "strings" "time" @@ -33,14 +34,14 @@ import ( // When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + if err := kc.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) + objectutil.FmtUnstructured(dryRunObject), err) } return kc.Apply(ctx, object, force) } @@ -55,7 +56,7 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr appliedObject := object.DeepCopy() if err := kc.apply(ctx, appliedObject); err != nil { - return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + return nil, fmt.Errorf("%s apply failed, error: %w", objectutil.FmtUnstructured(appliedObject), err) } if dryRunObject.GetResourceVersion() == "" { @@ -68,19 +69,19 @@ func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstr // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, // it applies the objects that are new or modified. func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { - sort.Sort(ApplyOrder(objects)) + sort.Sort(objectutil.ApplyOrder(objects)) changeSet := NewChangeSet() var toApply []*unstructured.Unstructured for _, object := range objects { existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { if force && strings.Contains(err.Error(), "immutable") { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { + if err := kc.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", - kc.fmt.Unstructured(dryRunObject), err) + objectutil.FmtUnstructured(dryRunObject), err) } return kc.ApplyAll(ctx, objects, force) } @@ -103,7 +104,7 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured for _, object := range toApply { appliedObject := object.DeepCopy() if err := kc.apply(ctx, appliedObject); err != nil { - return nil, fmt.Errorf("%s apply failed, error: %w", kc.fmt.Unstructured(appliedObject), err) + return nil, fmt.Errorf("%s apply failed, error: %w", objectutil.FmtUnstructured(appliedObject), err) } } @@ -124,7 +125,7 @@ func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstru var stageTwo []*unstructured.Unstructured for _, u := range objects { - if IsClusterDefinition(u.GetKind()) { + if objectutil.IsClusterDefinition(u.GetKind()) { stageOne = append(stageOne, u) } else { stageTwo = append(stageTwo, u) @@ -156,15 +157,15 @@ func (kc *ResourceManager) dryRunApply(ctx context.Context, object *unstructured opts := []client.PatchOption{ client.DryRunAll, client.ForceOwnership, - client.FieldOwner(kc.fieldOwner), + client.FieldOwner(kc.owner.Field), } - return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) + return kc.client.Patch(ctx, object, client.Apply, opts...) } func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { opts := []client.PatchOption{ client.ForceOwnership, - client.FieldOwner(kc.fieldOwner), + client.FieldOwner(kc.owner.Field), } - return kc.kubeClient.Patch(ctx, object, client.Apply, opts...) + return kc.client.Patch(ctx, object, client.Apply, opts...) } diff --git a/pkg/resmgr/manager_apply_test.go b/pkg/manager/manager_apply_test.go similarity index 92% rename from pkg/resmgr/manager_apply_test.go rename to pkg/manager/manager_apply_test.go index 335e6ac..47aa63d 100644 --- a/pkg/resmgr/manager_apply_test.go +++ b/pkg/manager/manager_apply_test.go @@ -1,9 +1,10 @@ -package resmgr +package manager import ( "context" "encoding/base64" "fmt" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "sort" "testing" "time" @@ -35,10 +36,10 @@ func TestApply(t *testing.T) { } // expected created order - sort.Sort(ApplyOrder(objects)) + sort.Sort(objectutil.ApplyOrder(objects)) var expected []string for _, object := range objects { - expected = append(expected, manager.fmt.Unstructured(object)) + expected = append(expected, objectutil.FmtUnstructured(object)) } // verify the change set contains only created actions @@ -67,7 +68,7 @@ func TestApply(t *testing.T) { var output []string for _, entry := range changeSet.Entries { if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) + t.Errorf("Mismatch from expected value (-want +got):\n%s\n%v", diff, changeSet) } output = append(output, entry.Subject) } @@ -101,7 +102,7 @@ func TestApply(t *testing.T) { // get the configmap from cluster configMapClone := configMap.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) + err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) if err != nil { t.Fatal(err) } @@ -161,7 +162,7 @@ func TestApply(t *testing.T) { } // verify that the error message does not contain sensitive information - expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", manager.fmt.Unstructured(secret)) + expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", objectutil.FmtUnstructured(secret)) if diff := cmp.Diff(expectedErr, err.Error()); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } @@ -189,7 +190,7 @@ func TestApply(t *testing.T) { // get the secret from cluster secretClone := secret.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) + err = manager.client.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) if err != nil { t.Fatal(err) } diff --git a/pkg/resmgr/manager_delete.go b/pkg/manager/manager_delete.go similarity index 76% rename from pkg/resmgr/manager_delete.go rename to pkg/manager/manager_delete.go index 8ae4319..c4aa4f6 100644 --- a/pkg/resmgr/manager_delete.go +++ b/pkg/manager/manager_delete.go @@ -15,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/objectutil" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" @@ -29,14 +30,14 @@ import ( // Delete deletes the given object (not found errors are ignored). func (kc *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + err := kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) if err != nil { if !apierrors.IsNotFound(err) { - return nil, fmt.Errorf("%s query failed, error: %w", kc.fmt.Unstructured(object), err) + return nil, fmt.Errorf("%s query failed, error: %w", objectutil.FmtUnstructured(object), err) } } else { - if err := kc.kubeClient.Delete(ctx, existingObject); err != nil { - return nil, fmt.Errorf("%s delete failed, error: %w", kc.fmt.Unstructured(object), err) + if err := kc.client.Delete(ctx, existingObject); err != nil { + return nil, fmt.Errorf("%s delete failed, error: %w", objectutil.FmtUnstructured(object), err) } } @@ -45,7 +46,7 @@ func (kc *ResourceManager) Delete(ctx context.Context, object *unstructured.Unst // DeleteAll deletes the given set of objects (not found errors are ignored).. func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { - sort.Sort(sort.Reverse(ApplyOrder(objects))) + sort.Sort(sort.Reverse(objectutil.ApplyOrder(objects))) changeSet := NewChangeSet() for _, object := range objects { diff --git a/pkg/resmgr/manager_delete_test.go b/pkg/manager/manager_delete_test.go similarity index 84% rename from pkg/resmgr/manager_delete_test.go rename to pkg/manager/manager_delete_test.go index 7390486..9264b42 100644 --- a/pkg/resmgr/manager_delete_test.go +++ b/pkg/manager/manager_delete_test.go @@ -1,7 +1,8 @@ -package resmgr +package manager import ( "context" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "testing" "time" @@ -40,7 +41,7 @@ func TestDelete(t *testing.T) { // expected deleted order var expected []string for _, object := range []*unstructured.Unstructured{configMap, binding} { - expected = append(expected, manager.fmt.Unstructured(object)) + expected = append(expected, objectutil.FmtUnstructured(object)) } // verify the change set contains only created actions @@ -58,13 +59,13 @@ func TestDelete(t *testing.T) { } configMapClone := configMap.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) + err = manager.client.Get(ctx, client.ObjectKeyFromObject(configMapClone), configMapClone) if !apierrors.IsNotFound(err) { t.Fatal(err) } bindingClone := binding.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(bindingClone), bindingClone) + err = manager.client.Get(ctx, client.ObjectKeyFromObject(bindingClone), bindingClone) if !apierrors.IsNotFound(err) { t.Fatal(err) } @@ -82,7 +83,7 @@ func TestDelete(t *testing.T) { } secretClone := secret.DeepCopy() - err = manager.kubeClient.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) + err = manager.client.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) if !apierrors.IsNotFound(err) { t.Fatal(err) } diff --git a/pkg/resmgr/manager_diff.go b/pkg/manager/manager_diff.go similarity index 87% rename from pkg/resmgr/manager_diff.go rename to pkg/manager/manager_diff.go index 763e57b..680cd4c 100644 --- a/pkg/resmgr/manager_diff.go +++ b/pkg/manager/manager_diff.go @@ -15,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "strings" "github.com/google/go-cmp/cmp" @@ -34,7 +35,7 @@ import ( // If the diff contains Kubernetes Secrets, the data values are masked. func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - _ = kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() if err := kc.dryRunApply(ctx, dryRunObject); err != nil { @@ -52,12 +53,12 @@ func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstru unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") if dryRunObject.GetKind() == "Secret" { - d, err := kc.fmt.MaskSecret(dryRunObject, "******") + d, err := objectutil.MaskSecret(dryRunObject, "******") if err != nil { return nil, fmt.Errorf("masking secret data failed, error: %w", err) } dryRunObject = d - ex, err := kc.fmt.MaskSecret(existingObject, "*****") + ex, err := objectutil.MaskSecret(existingObject, "*****") if err != nil { return nil, fmt.Errorf("masking secret data failed, error: %w", err) } @@ -110,7 +111,7 @@ func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured // if the error was caused by an invalid Kubernetes secrets. func (kc *ResourceManager) validationError(object *unstructured.Unstructured, err error) error { if apierrors.IsNotFound(err) { - return fmt.Errorf("%s namespace not specified, error: %w", kc.fmt.Unstructured(object), err) + return fmt.Errorf("%s namespace not specified, error: %w", objectutil.FmtUnstructured(object), err) } if object.GetKind() == "Secret" { @@ -118,9 +119,9 @@ func (kc *ResourceManager) validationError(object *unstructured.Unstructured, er if strings.Contains(err.Error(), "immutable") { msg = "secret is immutable" } - return fmt.Errorf("%s is invalid, error: %s", kc.fmt.Unstructured(object), msg) + return fmt.Errorf("%s is invalid, error: %s", objectutil.FmtUnstructured(object), msg) } - return fmt.Errorf("%s is invalid, error: %w", kc.fmt.Unstructured(object), err) + return fmt.Errorf("%s is invalid, error: %w", objectutil.FmtUnstructured(object), err) } diff --git a/pkg/resmgr/manager_diff_test.go b/pkg/manager/manager_diff_test.go similarity index 99% rename from pkg/resmgr/manager_diff_test.go rename to pkg/manager/manager_diff_test.go index b08c6a2..d375538 100644 --- a/pkg/resmgr/manager_diff_test.go +++ b/pkg/manager/manager_diff_test.go @@ -1,4 +1,4 @@ -package resmgr +package manager import ( "context" diff --git a/pkg/resmgr/manager_wait.go b/pkg/manager/manager_wait.go similarity index 92% rename from pkg/resmgr/manager_wait.go rename to pkg/manager/manager_wait.go index 7881425..e074874 100644 --- a/pkg/resmgr/manager_wait.go +++ b/pkg/manager/manager_wait.go @@ -15,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package manager import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "strings" "time" @@ -47,7 +48,7 @@ func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, PollInterval: interval, UseCache: true, } - eventsChan := kc.kstatusPoller.Poll(ctx, objectsMeta, opts) + eventsChan := kc.poller.Poll(ctx, objectsMeta, opts) lastStatus := make(map[object.ObjMetadata]*event.ResourceStatus) @@ -82,13 +83,13 @@ func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, var errors = []string{} for id, rs := range statusCollector.ResourceStatuses { if rs == nil { - errors = append(errors, fmt.Sprintf("can't determine status for %s", kc.fmt.ObjMetadata(id))) + errors = append(errors, fmt.Sprintf("can't determine status for %s", objectutil.FmtObjMetadata(id))) continue } if lastStatus[id].Status != status.CurrentStatus { var builder strings.Builder builder.WriteString(fmt.Sprintf("%s status: '%s'", - kc.fmt.ObjMetadata(rs.Identifier), lastStatus[id].Status)) + objectutil.FmtObjMetadata(rs.Identifier), lastStatus[id].Status)) if rs.Error != nil { builder.WriteString(fmt.Sprintf(": %s", rs.Error)) } @@ -117,7 +118,7 @@ func (kc *ResourceManager) WaitForTermination(objects []*unstructured.Unstructur func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { return func() (bool, error) { obj := object.DeepCopy() - err := kc.kubeClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) + err := kc.client.Get(ctx, client.ObjectKeyFromObject(obj), obj) if apierrors.IsNotFound(err) { return true, nil } diff --git a/pkg/manager/owner.go b/pkg/manager/owner.go new file mode 100644 index 0000000..c095962 --- /dev/null +++ b/pkg/manager/owner.go @@ -0,0 +1,30 @@ +/* +Copyright 2021 Stefan Prodan +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 manager + +// Owner contains options for setting the field manager and ownership labels group. +// The ownership labels are in the format: +// /name: +// /namespace: +type Owner struct { + // Field sets the field manager name for the given server-side apply patch. + Field string + + // Group sets the owner label key prefix. + Group string +} diff --git a/pkg/resmgr/testdata/test1.yaml b/pkg/manager/testdata/test1.yaml similarity index 100% rename from pkg/resmgr/testdata/test1.yaml rename to pkg/manager/testdata/test1.yaml diff --git a/pkg/objectutil/doc.go b/pkg/objectutil/doc.go new file mode 100644 index 0000000..643341e --- /dev/null +++ b/pkg/objectutil/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2021 Stefan Prodan +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 objectutil contains utilities for manipulating Kubernetes objects. +package objectutil diff --git a/pkg/resmgr/fmt.go b/pkg/objectutil/fmt.go similarity index 63% rename from pkg/resmgr/fmt.go rename to pkg/objectutil/fmt.go index 45a954d..b2d0eaa 100644 --- a/pkg/resmgr/fmt.go +++ b/pkg/objectutil/fmt.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package objectutil import ( "strings" @@ -26,33 +26,24 @@ import ( const fmtSeparator = "/" -type ResourceFormatter struct { - Separator string -} - -func (rf *ResourceFormatter) ObjMetadata(obj object.ObjMetadata) string { +// FmtObjMetadata returns the object ID in the format //. +func FmtObjMetadata(obj object.ObjMetadata) string { var builder strings.Builder - builder.WriteString(obj.GroupKind.Kind + rf.getSeparator()) + builder.WriteString(obj.GroupKind.Kind + fmtSeparator) if obj.Namespace != "" { - builder.WriteString(obj.Namespace + rf.getSeparator()) + builder.WriteString(obj.Namespace + fmtSeparator) } builder.WriteString(obj.Name) return builder.String() } -func (rf *ResourceFormatter) Unstructured(obj *unstructured.Unstructured) string { - return rf.ObjMetadata(object.UnstructuredToObjMeta(obj)) -} - -func (rf *ResourceFormatter) getSeparator() string { - if rf.Separator == "" { - return fmtSeparator - } - - return rf.Separator +// FmtUnstructured returns the object ID in the format //. +func FmtUnstructured(obj *unstructured.Unstructured) string { + return FmtObjMetadata(object.UnstructuredToObjMeta(obj)) } -func (rf *ResourceFormatter) MaskSecret(object *unstructured.Unstructured, mask string) (*unstructured.Unstructured, error) { +// MaskSecret replaces the data key values with the given mask. +func MaskSecret(object *unstructured.Unstructured, mask string) (*unstructured.Unstructured, error) { data, found, err := unstructured.NestedMap(object.Object, "data") if err != nil { return nil, err diff --git a/pkg/objectutil/io.go b/pkg/objectutil/io.go new file mode 100644 index 0000000..2cb6ee9 --- /dev/null +++ b/pkg/objectutil/io.go @@ -0,0 +1,109 @@ +/* +Copyright 2021 Stefan Prodan +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 objectutil + +import ( + "encoding/json" + "io" + "strings" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apiruntime "k8s.io/apimachinery/pkg/runtime" + yamlutil "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" +) + +// ReadObject decodes a YAML or JSON document from the given reader into an unstructured Kubernetes API object. +func ReadObject(r io.Reader) (*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + return nil, err + } + + return obj, nil +} + +// ReadObjects decodes the YAML or JSON documents from the given reader into unstructured Kubernetes API objects. +func ReadObjects(r io.Reader) ([]*unstructured.Unstructured, error) { + reader := yamlutil.NewYAMLOrJSONDecoder(r, 2048) + objects := make([]*unstructured.Unstructured, 0) + + for { + obj := &unstructured.Unstructured{} + err := reader.Decode(obj) + if err != nil { + if err == io.EOF { + err = nil + break + } + return objects, err + } + + if obj.IsList() { + err = obj.EachListItem(func(item apiruntime.Object) error { + obj := item.(*unstructured.Unstructured) + objects = append(objects, obj) + return nil + }) + if err != nil { + return objects, err + } + continue + } + + objects = append(objects, obj) + } + + return objects, nil +} + +// ObjectsToYAML encodes the given Kubernetes API objects to a YAML multi-doc. +func ObjectsToYAML(objects []*unstructured.Unstructured) (string, error) { + var builder strings.Builder + for _, obj := range objects { + data, err := yaml.Marshal(obj) + if err != nil { + return "", err + } + builder.Write(data) + builder.WriteString("---\n") + } + return builder.String(), nil +} + +// ObjectsToJSON encodes the given Kubernetes API objects to a YAML multi-doc. +func ObjectsToJSON(objects []*unstructured.Unstructured) (string, error) { + list := struct { + ApiVersion string `json:"apiVersion,omitempty"` + Kind string `json:"kind,omitempty"` + Items []*unstructured.Unstructured `json:"items,omitempty"` + }{ + ApiVersion: "v1", + Kind: "ListMeta", + Items: objects, + } + + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + return "", err + } + + return string(data), nil +} diff --git a/pkg/resmgr/sort.go b/pkg/objectutil/sort.go similarity index 94% rename from pkg/resmgr/sort.go rename to pkg/objectutil/sort.go index 631199a..5ea0de7 100644 --- a/pkg/resmgr/sort.go +++ b/pkg/objectutil/sort.go @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resmgr +package objectutil import ( "strings" @@ -25,7 +25,7 @@ import ( // ApplyOrder implements the Sort interface for Kubernetes objects based on kind. // When creating objects: CRDs, namespaces and other global kinds go first, while webhooks go last. -// When deleting objects: the order is inverted to allow the Kubernetes controllers to finalize custom resources. +// When deleting objects: the order should be inverted to allow the Kubernetes controllers to finalize custom resources. type ApplyOrder []*unstructured.Unstructured func (objects ApplyOrder) Len() int { From 2ce8667e5143663d8851511f8413d8f7d4fb2ec6 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 17:25:45 +0300 Subject: [PATCH 32/40] Add owner labels before apply Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 2 + cmd/kustomizer/diff.go | 4 ++ pkg/manager/manager.go | 26 +++++++------ pkg/manager/manager_apply.go | 64 +++++++++++++++---------------- pkg/manager/manager_apply_test.go | 2 + pkg/manager/manager_delete.go | 12 +++--- pkg/manager/manager_diff.go | 20 +++++----- pkg/manager/manager_wait.go | 12 +++--- pkg/manager/owner.go | 3 -- 9 files changed, 77 insertions(+), 68 deletions(-) diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 52cbc70..9385b05 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -99,6 +99,8 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { Group: PROJECT + ".dev", }) + resMgr.SetOwnerLabels(objects, applyArgs.inventoryName, applyArgs.inventoryNamespace) + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index 6e0c80c..f0a1ecb 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -89,6 +89,10 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() + if diffArgs.inventoryName != "" { + resMgr.SetOwnerLabels(objects, diffArgs.inventoryName, diffArgs.inventoryNamespace) + } + invalid := false for _, object := range objects { change, err := resMgr.Diff(ctx, object) diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index f8f8191..a5b350a 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -42,19 +42,23 @@ func NewResourceManager(client client.Client, poller *polling.StatusPoller, owne } // KubeClient returns the underlying controller-runtime client. -func (kc *ResourceManager) KubeClient() client.Client { - return kc.client +func (m *ResourceManager) KubeClient() client.Client { + return m.client } -func (kc *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { +func (m *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} } -//func (kc *ResourceManager) SetOwnerLabels(objects []*unstructured.Unstructured, name, namespace string) { -// for _, object := range objects { -// object.SetLabels(map[string]string{ -// kc.fieldOwner + "/name": name, -// kc.fieldOwner + "/namespace": namespace, -// }) -// } -//} +// SetOwnerLabels adds the ownership labels to the given objects. +// The ownership labels are in the format: +// /name: +// /namespace: +func (m *ResourceManager) SetOwnerLabels(objects []*unstructured.Unstructured, name, namespace string) { + for _, object := range objects { + object.SetLabels(map[string]string{ + m.owner.Group + "/name": name, + m.owner.Group + "/namespace": namespace, + }) + } +} diff --git a/pkg/manager/manager_apply.go b/pkg/manager/manager_apply.go index 7b7c174..7636d59 100644 --- a/pkg/manager/manager_apply.go +++ b/pkg/manager/manager_apply.go @@ -32,78 +32,78 @@ import ( // Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. // Drift detection is performed by comparing the server-side dry-run result with the existing object. // When immutable field changes are detected, the object is recreated if 'force' is set to 'true'. -func (kc *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { +func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstructured, force bool) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if err := m.dryRunApply(ctx, dryRunObject); err != nil { if force && strings.Contains(err.Error(), "immutable") { - if err := kc.client.Delete(ctx, existingObject); err != nil { + if err := m.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", objectutil.FmtUnstructured(dryRunObject), err) } - return kc.Apply(ctx, object, force) + return m.Apply(ctx, object, force) } - return nil, kc.validationError(dryRunObject, err) + return nil, m.validationError(dryRunObject, err) } // do not apply objects that have not drifted to avoid bumping the resource version - if !kc.hasDrifted(existingObject, dryRunObject) { - return kc.changeSetEntry(object, UnchangedAction), nil + if !m.hasDrifted(existingObject, dryRunObject) { + return m.changeSetEntry(object, UnchangedAction), nil } appliedObject := object.DeepCopy() - if err := kc.apply(ctx, appliedObject); err != nil { + if err := m.apply(ctx, appliedObject); err != nil { return nil, fmt.Errorf("%s apply failed, error: %w", objectutil.FmtUnstructured(appliedObject), err) } if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(appliedObject, CreatedAction), nil + return m.changeSetEntry(appliedObject, CreatedAction), nil } - return kc.changeSetEntry(appliedObject, ConfiguredAction), nil + return m.changeSetEntry(appliedObject, ConfiguredAction), nil } // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, // it applies the objects that are new or modified. -func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { +func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { sort.Sort(objectutil.ApplyOrder(objects)) changeSet := NewChangeSet() var toApply []*unstructured.Unstructured for _, object := range objects { existingObject := object.DeepCopy() - _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { + if err := m.dryRunApply(ctx, dryRunObject); err != nil { if force && strings.Contains(err.Error(), "immutable") { - if err := kc.client.Delete(ctx, existingObject); err != nil { + if err := m.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s immutable field detected, failed to delete object, error: %w", objectutil.FmtUnstructured(dryRunObject), err) } - return kc.ApplyAll(ctx, objects, force) + return m.ApplyAll(ctx, objects, force) } - return nil, kc.validationError(dryRunObject, err) + return nil, m.validationError(dryRunObject, err) } - if kc.hasDrifted(existingObject, dryRunObject) { + if m.hasDrifted(existingObject, dryRunObject) { toApply = append(toApply, object) if dryRunObject.GetResourceVersion() == "" { - changeSet.Add(*kc.changeSetEntry(dryRunObject, CreatedAction)) + changeSet.Add(*m.changeSetEntry(dryRunObject, CreatedAction)) } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, ConfiguredAction)) + changeSet.Add(*m.changeSetEntry(dryRunObject, ConfiguredAction)) } } else { - changeSet.Add(*kc.changeSetEntry(dryRunObject, UnchangedAction)) + changeSet.Add(*m.changeSetEntry(dryRunObject, UnchangedAction)) } } for _, object := range toApply { appliedObject := object.DeepCopy() - if err := kc.apply(ctx, appliedObject); err != nil { + if err := m.apply(ctx, appliedObject); err != nil { return nil, fmt.Errorf("%s apply failed, error: %w", objectutil.FmtUnstructured(appliedObject), err) } } @@ -115,7 +115,7 @@ func (kc *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured // waits for CRDs and Namespaces to become ready, then is applies all the other objects. // This function should be used when the given objects have a mix of custom resource definition and custom resources, // or a mix of namespace definitions with namespaced objects. -func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstructured.Unstructured, force bool, wait time.Duration) (*ChangeSet, error) { +func (m *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstructured.Unstructured, force bool, wait time.Duration) (*ChangeSet, error) { changeSet := NewChangeSet() // contains only CRDs and Namespaces @@ -133,18 +133,18 @@ func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstru } if len(stageOne) > 0 { - cs, err := kc.ApplyAll(ctx, stageOne, force) + cs, err := m.ApplyAll(ctx, stageOne, force) if err != nil { return nil, err } changeSet.Append(cs.Entries) - if err := kc.Wait(stageOne, 2*time.Second, wait); err != nil { + if err := m.Wait(stageOne, 2*time.Second, wait); err != nil { return nil, err } } - cs, err := kc.ApplyAll(ctx, stageTwo, force) + cs, err := m.ApplyAll(ctx, stageTwo, force) if err != nil { return nil, err } @@ -153,19 +153,19 @@ func (kc *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstru return changeSet, nil } -func (kc *ResourceManager) dryRunApply(ctx context.Context, object *unstructured.Unstructured) error { +func (m *ResourceManager) dryRunApply(ctx context.Context, object *unstructured.Unstructured) error { opts := []client.PatchOption{ client.DryRunAll, client.ForceOwnership, - client.FieldOwner(kc.owner.Field), + client.FieldOwner(m.owner.Field), } - return kc.client.Patch(ctx, object, client.Apply, opts...) + return m.client.Patch(ctx, object, client.Apply, opts...) } -func (kc *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { +func (m *ResourceManager) apply(ctx context.Context, object *unstructured.Unstructured) error { opts := []client.PatchOption{ client.ForceOwnership, - client.FieldOwner(kc.owner.Field), + client.FieldOwner(m.owner.Field), } - return kc.client.Patch(ctx, object, client.Apply, opts...) + return m.client.Patch(ctx, object, client.Apply, opts...) } diff --git a/pkg/manager/manager_apply_test.go b/pkg/manager/manager_apply_test.go index 47aa63d..7809fbb 100644 --- a/pkg/manager/manager_apply_test.go +++ b/pkg/manager/manager_apply_test.go @@ -25,6 +25,8 @@ func TestApply(t *testing.T) { t.Fatal(err) } + manager.SetOwnerLabels(objects, "app1", "default") + configMapName, configMap := getObjectFrom(objects, "ConfigMap", id) secretName, secret := getObjectFrom(objects, "Secret", id) diff --git a/pkg/manager/manager_delete.go b/pkg/manager/manager_delete.go index c4aa4f6..1f0459d 100644 --- a/pkg/manager/manager_delete.go +++ b/pkg/manager/manager_delete.go @@ -28,29 +28,29 @@ import ( ) // Delete deletes the given object (not found errors are ignored). -func (kc *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { +func (m *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - err := kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + err := m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) if err != nil { if !apierrors.IsNotFound(err) { return nil, fmt.Errorf("%s query failed, error: %w", objectutil.FmtUnstructured(object), err) } } else { - if err := kc.client.Delete(ctx, existingObject); err != nil { + if err := m.client.Delete(ctx, existingObject); err != nil { return nil, fmt.Errorf("%s delete failed, error: %w", objectutil.FmtUnstructured(object), err) } } - return kc.changeSetEntry(object, DeletedAction), nil + return m.changeSetEntry(object, DeletedAction), nil } // DeleteAll deletes the given set of objects (not found errors are ignored).. -func (kc *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { +func (m *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { sort.Sort(sort.Reverse(objectutil.ApplyOrder(objects))) changeSet := NewChangeSet() for _, object := range objects { - cse, err := kc.Delete(ctx, object) + cse, err := m.Delete(ctx, object) if err != nil { return nil, err } diff --git a/pkg/manager/manager_diff.go b/pkg/manager/manager_diff.go index 680cd4c..43d06bc 100644 --- a/pkg/manager/manager_diff.go +++ b/pkg/manager/manager_diff.go @@ -33,21 +33,21 @@ import ( // Diff performs a server-side apply dry-un and returns the fields that changed in YAML format. // If the diff contains Kubernetes Secrets, the data values are masked. -func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { +func (m *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstructured) (*ChangeSetEntry, error) { existingObject := object.DeepCopy() - _ = kc.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) + _ = m.client.Get(ctx, client.ObjectKeyFromObject(object), existingObject) dryRunObject := object.DeepCopy() - if err := kc.dryRunApply(ctx, dryRunObject); err != nil { - return nil, kc.validationError(dryRunObject, err) + if err := m.dryRunApply(ctx, dryRunObject); err != nil { + return nil, m.validationError(dryRunObject, err) } if dryRunObject.GetResourceVersion() == "" { - return kc.changeSetEntry(dryRunObject, CreatedAction), nil + return m.changeSetEntry(dryRunObject, CreatedAction), nil } - if kc.hasDrifted(existingObject, dryRunObject) { - cse := kc.changeSetEntry(object, ConfiguredAction) + if m.hasDrifted(existingObject, dryRunObject) { + cse := m.changeSetEntry(object, ConfiguredAction) unstructured.RemoveNestedField(dryRunObject.Object, "metadata", "managedFields") unstructured.RemoveNestedField(existingObject.Object, "metadata", "managedFields") @@ -72,11 +72,11 @@ func (kc *ResourceManager) Diff(ctx context.Context, object *unstructured.Unstru return cse, nil } - return kc.changeSetEntry(dryRunObject, UnchangedAction), nil + return m.changeSetEntry(dryRunObject, UnchangedAction), nil } // hasDrifted detects changes to metadata labels, metadata annotations, spec and webhooks. -func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { +func (m *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured.Unstructured) bool { if dryRunObject.GetResourceVersion() == "" { return true } @@ -109,7 +109,7 @@ func (kc *ResourceManager) hasDrifted(existingObject, dryRunObject *unstructured // validationError formats the given error and hides sensitive data // if the error was caused by an invalid Kubernetes secrets. -func (kc *ResourceManager) validationError(object *unstructured.Unstructured, err error) error { +func (m *ResourceManager) validationError(object *unstructured.Unstructured, err error) error { if apierrors.IsNotFound(err) { return fmt.Errorf("%s namespace not specified, error: %w", objectutil.FmtUnstructured(object), err) } diff --git a/pkg/manager/manager_wait.go b/pkg/manager/manager_wait.go index e074874..c21bf3a 100644 --- a/pkg/manager/manager_wait.go +++ b/pkg/manager/manager_wait.go @@ -37,7 +37,7 @@ import ( ) // Wait checks if the given set of objects has been fully reconciled. -func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { +func (m *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { objectsMeta := object.UnstructuredsToObjMetas(objects) statusCollector := collector.NewResourceStatusCollector(objectsMeta) @@ -48,7 +48,7 @@ func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, PollInterval: interval, UseCache: true, } - eventsChan := kc.poller.Poll(ctx, objectsMeta, opts) + eventsChan := m.poller.Poll(ctx, objectsMeta, opts) lastStatus := make(map[object.ObjMetadata]*event.ResourceStatus) @@ -103,22 +103,22 @@ func (kc *ResourceManager) Wait(objects []*unstructured.Unstructured, interval, } // WaitForTermination waits for the given objects to be deleted from the cluster. -func (kc *ResourceManager) WaitForTermination(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { +func (m *ResourceManager) WaitForTermination(objects []*unstructured.Unstructured, interval, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() for _, object := range objects { - if err := wait.PollImmediate(interval, timeout, kc.isDeleted(ctx, object)); err != nil { + if err := wait.PollImmediate(interval, timeout, m.isDeleted(ctx, object)); err != nil { return err } } return nil } -func (kc *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { +func (m *ResourceManager) isDeleted(ctx context.Context, object *unstructured.Unstructured) wait.ConditionFunc { return func() (bool, error) { obj := object.DeepCopy() - err := kc.client.Get(ctx, client.ObjectKeyFromObject(obj), obj) + err := m.client.Get(ctx, client.ObjectKeyFromObject(obj), obj) if apierrors.IsNotFound(err) { return true, nil } diff --git a/pkg/manager/owner.go b/pkg/manager/owner.go index c095962..450b700 100644 --- a/pkg/manager/owner.go +++ b/pkg/manager/owner.go @@ -18,9 +18,6 @@ limitations under the License. package manager // Owner contains options for setting the field manager and ownership labels group. -// The ownership labels are in the format: -// /name: -// /namespace: type Owner struct { // Field sets the field manager name for the given server-side apply patch. Field string From d2edcb08c8e8df36b00558ae97803d7db1de552c Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 18:35:11 +0300 Subject: [PATCH 33/40] Refactor inventory manager Signed-off-by: Stefan Prodan --- cmd/kustomizer/apply.go | 9 +- cmd/kustomizer/delete.go | 13 ++- cmd/kustomizer/diff.go | 9 +- cmd/kustomizer/main.go | 22 ++--- pkg/inventory/doc.go | 5 -- pkg/inventory/manager.go | 146 ------------------------------- pkg/manager/doc.go | 2 + pkg/manager/manager.go | 14 +-- pkg/manager/manager_inventory.go | 128 +++++++++++++++++++++++++++ 9 files changed, 157 insertions(+), 191 deletions(-) delete mode 100644 pkg/inventory/manager.go create mode 100644 pkg/manager/manager_inventory.go diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 9385b05..d579e35 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -94,10 +94,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) resMgr.SetOwnerLabels(objects, applyArgs.inventoryName, applyArgs.inventoryNamespace) @@ -133,12 +130,12 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("mode not supported") } - staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + staleObjects, err := resMgr.GetInventoryStaleObjects(ctx, newInventory) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } - err = inventoryMgr.Store(ctx, resMgr.KubeClient(), newInventory, applyArgs.inventoryName, applyArgs.inventoryNamespace) + err = resMgr.ApplyInventory(ctx, newInventory) if err != nil { return fmt.Errorf("inventory apply failed, error: %w", err) } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index e2f0554..ffdc768 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "github.com/stefanprodan/kustomizer/pkg/inventory" "os" "sort" "time" @@ -74,13 +75,10 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) - inv, err := inventoryMgr.Retrieve(ctx, kubeClient, deleteArgs.inventoryName, deleteArgs.inventoryNamespace) - if err != nil { + inv := inventory.NewInventory(deleteArgs.inventoryName, deleteArgs.inventoryNamespace) + if err := resMgr.GetInventory(ctx, inv); err != nil { return err } @@ -106,8 +104,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { os.Exit(1) } - err = inventoryMgr.Remove(ctx, resMgr.KubeClient(), deleteArgs.inventoryName, deleteArgs.inventoryNamespace) - if err != nil { + if err := resMgr.DeleteInventory(ctx, inv); err != nil { return err } diff --git a/cmd/kustomizer/diff.go b/cmd/kustomizer/diff.go index f0a1ecb..81a509d 100644 --- a/cmd/kustomizer/diff.go +++ b/cmd/kustomizer/diff.go @@ -66,7 +66,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { return err } - newInventory := inventory.NewInventory(applyArgs.inventoryName, applyArgs.inventoryNamespace) + newInventory := inventory.NewInventory(diffArgs.inventoryName, diffArgs.inventoryNamespace) if err := newInventory.AddObjects(objects); err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } @@ -81,10 +81,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("status poller init failed: %w", err) } - resMgr := manager.NewResourceManager(kubeClient, statusPoller, manager.Owner{ - Field: PROJECT, - Group: PROJECT + ".dev", - }) + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -114,7 +111,7 @@ func runDiffCmd(cmd *cobra.Command, args []string) error { } if diffArgs.inventoryName != "" { - staleObjects, err := inventoryMgr.GetStaleObjects(ctx, resMgr.KubeClient(), newInventory, diffArgs.inventoryName, diffArgs.inventoryNamespace) + staleObjects, err := resMgr.GetInventoryStaleObjects(ctx, newInventory) if err != nil { return fmt.Errorf("inventory query failed, error: %w", err) } diff --git a/cmd/kustomizer/main.go b/cmd/kustomizer/main.go index 6bb3883..1558fc9 100644 --- a/cmd/kustomizer/main.go +++ b/cmd/kustomizer/main.go @@ -17,14 +17,13 @@ limitations under the License. package main import ( + "github.com/stefanprodan/kustomizer/pkg/manager" "os" - "path/filepath" + "path" "time" "github.com/spf13/cobra" _ "k8s.io/client-go/plugin/pkg/client/auth" - - "github.com/stefanprodan/kustomizer/pkg/inventory" ) var VERSION = "1.0.0-dev.0" @@ -46,9 +45,12 @@ type rootFlags struct { } var ( - rootArgs = rootFlags{} - logger = stderrLogger{stderr: os.Stderr} - inventoryMgr *inventory.InventoryManager + rootArgs = rootFlags{} + logger = stderrLogger{stderr: os.Stderr} + inventoryOwner = manager.Owner{ + Field: "kustomizer", + Group: "inventory.kustomizer.dev", + } ) func init() { @@ -65,12 +67,6 @@ func init() { func main() { configureKubeconfig() - if im, err := inventory.NewInventoryManager(PROJECT, PROJECT+".dev"); err != nil { - panic(err) - } else { - inventoryMgr = im - } - if err := rootCmd.Execute(); err != nil { logger.Println(`✗`, err) os.Exit(1) @@ -84,7 +80,7 @@ func configureKubeconfig() { rootArgs.kubeconfig = os.Getenv("KUBECONFIG") default: if home := homeDir(); len(home) > 0 { - rootArgs.kubeconfig = filepath.Join(home, ".kube", "config") + rootArgs.kubeconfig = path.Join(home, ".kube", "config") } } } diff --git a/pkg/inventory/doc.go b/pkg/inventory/doc.go index c83ee0a..fc01cfd 100644 --- a/pkg/inventory/doc.go +++ b/pkg/inventory/doc.go @@ -16,9 +16,4 @@ limitations under the License. */ // Package inventory contains utilities for keeping a record of Kubernetes objects applied on a cluster. -// -// The InventoryManager performs the following actions: -// - records the Kubernetes objects metadata in a compacted format -// - stores the inventory in a Kubernetes ConfigMap -// - determines which objects are subject to garbage collection package inventory diff --git a/pkg/inventory/manager.go b/pkg/inventory/manager.go deleted file mode 100644 index 5d34fd5..0000000 --- a/pkg/inventory/manager.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2021 Stefan Prodan -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 inventory - -import ( - "context" - "encoding/json" - "fmt" - "time" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// InventoryManager records the Kubernetes objects that are applied on the cluster. -type InventoryManager struct { - fieldOwner string - group string -} - -// NewInventoryManager returns an InventoryManager. -func NewInventoryManager(fieldOwner, group string) (*InventoryManager, error) { - if fieldOwner == "" { - return nil, fmt.Errorf("fieldOwner is required") - } - if group == "" { - return nil, fmt.Errorf("group is required") - } - - return &InventoryManager{ - fieldOwner: fieldOwner, - group: group, - }, nil -} - -// Store applies the Inventory object on the server. -func (im *InventoryManager) Store(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) error { - data, err := json.Marshal(inv.Entries) - if err != nil { - return err - } - - cm := im.newConfigMap(name, namespace) - cm.Annotations = map[string]string{ - im.group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), - } - cm.Data = map[string]string{ - "inventory": string(data), - } - - opts := []client.PatchOption{ - client.ForceOwnership, - client.FieldOwner(im.fieldOwner), - } - return kubeClient.Patch(ctx, cm, client.Apply, opts...) -} - -// Retrieve fetches the Inventory object from the server. -func (im *InventoryManager) Retrieve(ctx context.Context, kubeClient client.Client, name, namespace string) (*Inventory, error) { - cm := im.newConfigMap(name, namespace) - - cmKey := client.ObjectKeyFromObject(cm) - err := kubeClient.Get(ctx, cmKey, cm) - if err != nil { - return nil, err - } - - if _, ok := cm.Data["inventory"]; !ok { - return nil, fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) - } - - var entries []Entry - err = json.Unmarshal([]byte(cm.Data["inventory"]), &entries) - if err != nil { - return nil, err - } - - return &Inventory{Entries: entries}, nil -} - -// GetStaleObjects returns the list of objects subject to pruning. -func (im *InventoryManager) GetStaleObjects(ctx context.Context, kubeClient client.Client, inv *Inventory, name, namespace string) ([]*unstructured.Unstructured, error) { - objects := make([]*unstructured.Unstructured, 0) - exInv, err := im.Retrieve(ctx, kubeClient, name, namespace) - if err != nil { - if apierrors.IsNotFound(err) { - return objects, nil - } - return nil, err - } - - objects, err = exInv.Diff(inv) - if err != nil { - return nil, err - } - - return objects, nil -} - -// Remove deletes the Inventory object from the server. -func (im *InventoryManager) Remove(ctx context.Context, kubeClient client.Client, name, namespace string) error { - cm := im.newConfigMap(name, namespace) - - cmKey := client.ObjectKeyFromObject(cm) - err := kubeClient.Delete(ctx, cm) - if err != nil && !apierrors.IsNotFound(err) { - return fmt.Errorf("failed to delete ConfigMap/%s, error: %w", cmKey, err) - } - return nil -} - -func (im *InventoryManager) newConfigMap(name, namespace string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "ConfigMap", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Labels: map[string]string{ - "app.kubernetes.io/name": name, - "app.kubernetes.io/component": "inventory", - "app.kubernetes.io/created-by": im.fieldOwner, - }, - }, - } -} diff --git a/pkg/manager/doc.go b/pkg/manager/doc.go index 8fdbac3..2b580f8 100644 --- a/pkg/manager/doc.go +++ b/pkg/manager/doc.go @@ -25,4 +25,6 @@ limitations under the License. // - waits for the objects to be fully reconciled by looking up their readiness status // - deletes objects that are subject to garbage collection // - waits for the deleted objects to be terminated +// - maintains an inventory of objects applied on the cluster +// - performs garbage collection of stale objects based on the inventory entries package manager diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go index a5b350a..9c50dd3 100644 --- a/pkg/manager/manager.go +++ b/pkg/manager/manager.go @@ -32,7 +32,7 @@ type ResourceManager struct { owner Owner } -// NewResourceManager creates a ResourceManager for the given Kubernetes client config and context. +// NewResourceManager creates a ResourceManager for the given Kubernetes client. func NewResourceManager(client client.Client, poller *polling.StatusPoller, owner Owner) *ResourceManager { return &ResourceManager{ client: client, @@ -41,15 +41,11 @@ func NewResourceManager(client client.Client, poller *polling.StatusPoller, owne } } -// KubeClient returns the underlying controller-runtime client. -func (m *ResourceManager) KubeClient() client.Client { +// Client returns the underlying controller-runtime client. +func (m *ResourceManager) Client() client.Client { return m.client } -func (m *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { - return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} -} - // SetOwnerLabels adds the ownership labels to the given objects. // The ownership labels are in the format: // /name: @@ -62,3 +58,7 @@ func (m *ResourceManager) SetOwnerLabels(objects []*unstructured.Unstructured, n }) } } + +func (m *ResourceManager) changeSetEntry(object *unstructured.Unstructured, action Action) *ChangeSetEntry { + return &ChangeSetEntry{Subject: objectutil.FmtUnstructured(object), Action: string(action)} +} diff --git a/pkg/manager/manager_inventory.go b/pkg/manager/manager_inventory.go new file mode 100644 index 0000000..c700e52 --- /dev/null +++ b/pkg/manager/manager_inventory.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 Stefan Prodan +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 manager + +import ( + "context" + "encoding/json" + "fmt" + "github.com/stefanprodan/kustomizer/pkg/inventory" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "time" +) + +const inventoryKindName = "inventory" + +// ApplyInventory creates or updates the ConfigMap object for the given inventory. +func (m *ResourceManager) ApplyInventory(ctx context.Context, i *inventory.Inventory) error { + data, err := json.Marshal(i.Entries) + if err != nil { + return err + } + + cm := m.newConfigMap(i.Name, i.Namespace) + cm.Annotations = map[string]string{ + m.owner.Group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), + } + cm.Data = map[string]string{ + inventoryKindName: string(data), + } + + opts := []client.PatchOption{ + client.ForceOwnership, + client.FieldOwner(m.owner.Field), + } + return m.client.Patch(ctx, cm, client.Apply, opts...) +} + +// GetInventory retrieves the entries from the ConfigMap for the given inventory name and namespace. +func (m *ResourceManager) GetInventory(ctx context.Context, i *inventory.Inventory) error { + cm := m.newConfigMap(i.Name, i.Namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := m.client.Get(ctx, cmKey, cm) + if err != nil { + return err + } + + if _, ok := cm.Data[inventoryKindName]; !ok { + return fmt.Errorf("inventory data not found in ConfigMap/%s", cmKey) + } + + var entries []inventory.Entry + err = json.Unmarshal([]byte(cm.Data[inventoryKindName]), &entries) + if err != nil { + return err + } + + i.Entries = entries + return nil +} + +// DeleteInventory removes the ConfigMap for the given inventory name and namespace. +func (m *ResourceManager) DeleteInventory(ctx context.Context, i *inventory.Inventory) error { + cm := m.newConfigMap(i.Name, i.Namespace) + + cmKey := client.ObjectKeyFromObject(cm) + err := m.client.Delete(ctx, cm) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to delete ConfigMap/%s, error: %w", cmKey, err) + } + return nil +} + +// GetInventoryStaleObjects returns the list of objects subject to pruning. +func (m *ResourceManager) GetInventoryStaleObjects(ctx context.Context, i *inventory.Inventory) ([]*unstructured.Unstructured, error) { + objects := make([]*unstructured.Unstructured, 0) + existingInventory := inventory.NewInventory(i.Name, i.Namespace) + if err := m.GetInventory(ctx, existingInventory); err != nil { + if apierrors.IsNotFound(err) { + return objects, nil + } + return nil, err + } + + objects, err := existingInventory.Diff(i) + if err != nil { + return nil, err + } + + return objects, nil +} + +func (m *ResourceManager) newConfigMap(name, namespace string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": name, + "app.kubernetes.io/component": inventoryKindName, + "app.kubernetes.io/created-by": m.owner.Field, + }, + }, + } +} From 225df6e0e739d41dcf0784178836ecf62f2a37f8 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 3 Sep 2021 20:54:31 +0300 Subject: [PATCH 34/40] Add benchmark for apply Signed-off-by: Stefan Prodan --- Makefile | 3 + pkg/manager/main_test.go | 2 +- pkg/manager/manager_apply_bench_test.go | 132 ++++++++++++++++++++++++ pkg/manager/manager_apply_test.go | 4 +- pkg/manager/manager_delete_test.go | 8 +- pkg/manager/manager_diff_test.go | 4 +- 6 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 pkg/manager/manager_apply_bench_test.go diff --git a/Makefile b/Makefile index 85bd97b..ad37944 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ test: tidy fmt vet install-envtest test-race: tidy fmt vet install-envtest KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -v -race -parallel 4 -coverprofile cover.out +test-bench: + KUBEBUILDER_ASSETS=$(KUBEBUILDER_ASSETS) go test ./... -v -bench=. -run=none + ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else diff --git a/pkg/manager/main_test.go b/pkg/manager/main_test.go index f840e35..180f715 100644 --- a/pkg/manager/main_test.go +++ b/pkg/manager/main_test.go @@ -93,7 +93,7 @@ func generateName(prefix string) string { return fmt.Sprintf("%s-%d", prefix, id) } -func getObjectFrom(objects []*unstructured.Unstructured, kind, name string) (string, *unstructured.Unstructured) { +func getFirstObject(objects []*unstructured.Unstructured, kind, name string) (string, *unstructured.Unstructured) { for _, object := range objects { if object.GetKind() == kind && object.GetName() == name { return objectutil.FmtUnstructured(object), object diff --git a/pkg/manager/manager_apply_bench_test.go b/pkg/manager/manager_apply_bench_test.go new file mode 100644 index 0000000..3784812 --- /dev/null +++ b/pkg/manager/manager_apply_bench_test.go @@ -0,0 +1,132 @@ +/* +Copyright 2021 Stefan Prodan +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 manager + +import ( + "context" + "testing" + "time" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func BenchmarkTestApply10(b *testing.B) { + for i := 0; i < b.N; i++ { + timeout := 10 * time.Second + + id := generateName("bench") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + panic(err) + } + + _, err = manager.ApplyAllStaged(context.Background(), objects, false, timeout) + if err != nil { + panic(err) + } + } +} + +func BenchmarkTestApplyWait10(b *testing.B) { + for i := 0; i < b.N; i++ { + timeout := 10 * time.Second + + id := generateName("bench") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + b.Fatal(err) + } + + if _, err = manager.ApplyAllStaged(context.Background(), objects, false, timeout); err != nil { + b.Fatal(err) + } + + if err := manager.Wait(objects, time.Second, 5*time.Second); err != nil { + b.Error(err) + } + } +} + +func BenchmarkTestApplyDelete10(b *testing.B) { + for i := 0; i < b.N; i++ { + timeout := 10 * time.Second + id := generateName("bench") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + b.Fatal(err) + } + + if _, err = manager.ApplyAllStaged(context.Background(), objects, false, timeout); err != nil { + b.Fatal(err) + } + + if _, err := manager.DeleteAll(context.Background(), objects); err != nil { + b.Error(err) + } + } +} + +func BenchmarkTestApplyDeleteWait10(b *testing.B) { + for i := 0; i < b.N; i++ { + timeout := 10 * time.Second + id := generateName("bench") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + b.Fatal(err) + } + + if _, err = manager.ApplyAllStaged(context.Background(), objects, false, timeout); err != nil { + b.Fatal(err) + } + + if _, err := manager.DeleteAll(context.Background(), objects); err != nil { + b.Error(err) + } + + _, configmap := getFirstObject(objects, "ConfigMap", id) + _, role := getFirstObject(objects, "ClusterRole", id) + if err := manager.WaitForTermination([]*unstructured.Unstructured{role, configmap}, time.Second, timeout); err != nil { + b.Error(err) + } + } +} + +func BenchmarkTestApplyDryRun10(b *testing.B) { + for i := 0; i < b.N; i++ { + id := generateName("bench") + objects, err := readManifest("testdata/test1.yaml", id) + if err != nil { + panic(err) + } + + _, role := getFirstObject(objects, "ClusterRole", id) + if _, err := manager.Diff(context.Background(), role); err != nil { + b.Error(err) + } + + _, roleb := getFirstObject(objects, "ClusterRoleBinding", id) + if _, err := manager.Diff(context.Background(), roleb); err != nil { + b.Error(err) + } + + _, ns := getFirstObject(objects, "Namespace", id) + if _, err := manager.Diff(context.Background(), ns); err != nil { + b.Error(err) + } + } +} diff --git a/pkg/manager/manager_apply_test.go b/pkg/manager/manager_apply_test.go index 7809fbb..b515b4f 100644 --- a/pkg/manager/manager_apply_test.go +++ b/pkg/manager/manager_apply_test.go @@ -27,8 +27,8 @@ func TestApply(t *testing.T) { manager.SetOwnerLabels(objects, "app1", "default") - configMapName, configMap := getObjectFrom(objects, "ConfigMap", id) - secretName, secret := getObjectFrom(objects, "Secret", id) + configMapName, configMap := getFirstObject(objects, "ConfigMap", id) + secretName, secret := getFirstObject(objects, "Secret", id) t.Run("creates objects in order", func(t *testing.T) { // create objects diff --git a/pkg/manager/manager_delete_test.go b/pkg/manager/manager_delete_test.go index 9264b42..733a586 100644 --- a/pkg/manager/manager_delete_test.go +++ b/pkg/manager/manager_delete_test.go @@ -23,10 +23,10 @@ func TestDelete(t *testing.T) { t.Fatal(err) } - _, role := getObjectFrom(objects, "ClusterRole", id) - _, binding := getObjectFrom(objects, "ClusterRoleBinding", id) - _, configMap := getObjectFrom(objects, "ConfigMap", id) - _, secret := getObjectFrom(objects, "Secret", id) + _, role := getFirstObject(objects, "ClusterRole", id) + _, binding := getFirstObject(objects, "ClusterRoleBinding", id) + _, configMap := getFirstObject(objects, "ConfigMap", id) + _, secret := getFirstObject(objects, "Secret", id) if _, err = manager.ApplyAllStaged(ctx, objects, false, timeout); err != nil { t.Fatal(err) diff --git a/pkg/manager/manager_diff_test.go b/pkg/manager/manager_diff_test.go index d375538..25a3280 100644 --- a/pkg/manager/manager_diff_test.go +++ b/pkg/manager/manager_diff_test.go @@ -21,8 +21,8 @@ func TestDiff(t *testing.T) { t.Fatal(err) } - configMapName, configMap := getObjectFrom(objects, "ConfigMap", id) - secretName, secret := getObjectFrom(objects, "Secret", id) + configMapName, configMap := getFirstObject(objects, "ConfigMap", id) + secretName, secret := getFirstObject(objects, "Secret", id) if err := unstructured.SetNestedField(secret.Object, false, "immutable"); err != nil { t.Fatal(err) From b7067df00afbfc0c16ebb0d289068f029587da40 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Sun, 5 Sep 2021 21:22:44 +0300 Subject: [PATCH 35/40] Use cli-utils/pkg/ordering Signed-off-by: Stefan Prodan --- cmd/kustomizer/build.go | 3 +- cmd/kustomizer/delete.go | 6 +-- pkg/inventory/inventory.go | 18 ++++--- pkg/manager/manager_apply.go | 17 +++++-- pkg/manager/manager_apply_test.go | 7 +-- pkg/manager/manager_delete.go | 8 ++-- pkg/manager/manager_delete_test.go | 30 +++++------- pkg/manager/manager_diff.go | 15 ++++-- pkg/manager/manager_wait.go | 5 +- pkg/objectutil/io.go | 11 ++++- pkg/objectutil/sort.go | 75 ------------------------------ 11 files changed, 76 insertions(+), 119 deletions(-) delete mode 100644 pkg/objectutil/sort.go diff --git a/cmd/kustomizer/build.go b/cmd/kustomizer/build.go index 29de4f2..2394093 100644 --- a/cmd/kustomizer/build.go +++ b/cmd/kustomizer/build.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/kustomize/api/krusty" kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filesys" @@ -125,7 +126,7 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U } } - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) return objects, nil } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index ffdc768..e2fdff9 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -19,15 +19,15 @@ package main import ( "context" "fmt" - "github.com/stefanprodan/kustomizer/pkg/inventory" "os" "sort" "time" "github.com/spf13/cobra" + "sigs.k8s.io/cli-utils/pkg/ordering" + "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/manager" - "github.com/stefanprodan/kustomizer/pkg/objectutil" ) var deleteCmd = &cobra.Command{ @@ -89,7 +89,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { logger.Println(fmt.Sprintf("deleting %v manifest(s)...", len(objects))) hasErrors := false - sort.Sort(sort.Reverse(objectutil.ApplyOrder(objects))) + sort.Sort(sort.Reverse(ordering.SortableUnstructureds(objects))) for _, object := range objects { change, err := resMgr.Delete(ctx, object) if err != nil { diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index 9006b07..eed3869 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -23,8 +23,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/object" - - "github.com/stefanprodan/kustomizer/pkg/objectutil" + "sigs.k8s.io/cli-utils/pkg/ordering" ) // Inventory is a record of objects that are applied on a cluster stored as a configmap. @@ -39,9 +38,14 @@ type Inventory struct { Entries []Entry `json:"entries"` } -// Entry is a record of a Kubernetes object metadata. +// Entry contains the information necessary to locate the +// resource within a cluster. type Entry struct { - ObjectID string `json:"id"` + // ObjectID is the string representation of object.ObjMetadata, + // in the format '___'. + ObjectID string `json:"id"` + + // ObjectVersion is the API version of this entry kind. ObjectVersion string `json:"ver"` } @@ -55,7 +59,7 @@ func NewInventory(name, namespace string) *Inventory { // AddObjects extracts the metadata from the given objects and adds it to the inventory. func (inv *Inventory) AddObjects(objects []*unstructured.Unstructured) error { - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) for _, om := range objects { objMetadata := object.UnstructuredToObjMeta(om) gv, err := schema.ParseGroupVersion(om.GetAPIVersion()) @@ -103,7 +107,7 @@ func (inv *Inventory) ListObjects() ([]*unstructured.Unstructured, error) { objects = append(objects, u) } - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) return objects, nil } @@ -151,6 +155,6 @@ func (inv *Inventory) Diff(target *Inventory) ([]*unstructured.Unstructured, err objects = append(objects, u) } - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) return objects, nil } diff --git a/pkg/manager/manager_apply.go b/pkg/manager/manager_apply.go index 7636d59..4bf24a2 100644 --- a/pkg/manager/manager_apply.go +++ b/pkg/manager/manager_apply.go @@ -20,13 +20,15 @@ package manager import ( "context" "fmt" - "github.com/stefanprodan/kustomizer/pkg/objectutil" "sort" "strings" "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/stefanprodan/kustomizer/pkg/objectutil" ) // Apply performs a server-side apply of the given object if the matching in-cluster object is different or if it doesn't exist. @@ -69,7 +71,7 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, // it applies the objects that are new or modified. func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) changeSet := NewChangeSet() var toApply []*unstructured.Unstructured for _, object := range objects { @@ -125,7 +127,7 @@ func (m *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstruc var stageTwo []*unstructured.Unstructured for _, u := range objects { - if objectutil.IsClusterDefinition(u.GetKind()) { + if m.isClusterDefinition(u.GetKind()) { stageOne = append(stageOne, u) } else { stageTwo = append(stageTwo, u) @@ -169,3 +171,12 @@ func (m *ResourceManager) apply(ctx context.Context, object *unstructured.Unstru } return m.client.Patch(ctx, object, client.Apply, opts...) } + +func (m *ResourceManager) isClusterDefinition(kind string) bool { + switch strings.ToLower(kind) { + case "customresourcedefinition": + case "namespace": + return true + } + return false +} diff --git a/pkg/manager/manager_apply_test.go b/pkg/manager/manager_apply_test.go index b515b4f..cc38948 100644 --- a/pkg/manager/manager_apply_test.go +++ b/pkg/manager/manager_apply_test.go @@ -4,13 +4,14 @@ import ( "context" "encoding/base64" "fmt" - "github.com/stefanprodan/kustomizer/pkg/objectutil" "sort" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/stefanprodan/kustomizer/pkg/objectutil" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -38,7 +39,7 @@ func TestApply(t *testing.T) { } // expected created order - sort.Sort(objectutil.ApplyOrder(objects)) + sort.Sort(ordering.SortableUnstructureds(objects)) var expected []string for _, object := range objects { expected = append(expected, objectutil.FmtUnstructured(object)) @@ -164,7 +165,7 @@ func TestApply(t *testing.T) { } // verify that the error message does not contain sensitive information - expectedErr := fmt.Sprintf("%s is invalid, error: secret is immutable", objectutil.FmtUnstructured(secret)) + expectedErr := fmt.Sprintf("%s invalid, error: secret is immutable", objectutil.FmtUnstructured(secret)) if diff := cmp.Diff(expectedErr, err.Error()); diff != "" { t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) } diff --git a/pkg/manager/manager_delete.go b/pkg/manager/manager_delete.go index 1f0459d..4d567a3 100644 --- a/pkg/manager/manager_delete.go +++ b/pkg/manager/manager_delete.go @@ -20,11 +20,13 @@ package manager import ( "context" "fmt" + "sort" + "github.com/stefanprodan/kustomizer/pkg/objectutil" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" - "sort" ) // Delete deletes the given object (not found errors are ignored). @@ -44,9 +46,9 @@ func (m *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstr return m.changeSetEntry(object, DeletedAction), nil } -// DeleteAll deletes the given set of objects (not found errors are ignored).. +// DeleteAll deletes the given set of objects (not found errors are ignored). func (m *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { - sort.Sort(sort.Reverse(objectutil.ApplyOrder(objects))) + sort.Sort(sort.Reverse(ordering.SortableUnstructureds(objects))) changeSet := NewChangeSet() for _, object := range objects { diff --git a/pkg/manager/manager_delete_test.go b/pkg/manager/manager_delete_test.go index 733a586..4560160 100644 --- a/pkg/manager/manager_delete_test.go +++ b/pkg/manager/manager_delete_test.go @@ -3,12 +3,12 @@ package manager import ( "context" "github.com/stefanprodan/kustomizer/pkg/objectutil" + "strings" "testing" "time" "github.com/google/go-cmp/cmp" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -23,24 +23,22 @@ func TestDelete(t *testing.T) { t.Fatal(err) } - _, role := getFirstObject(objects, "ClusterRole", id) - _, binding := getFirstObject(objects, "ClusterRoleBinding", id) _, configMap := getFirstObject(objects, "ConfigMap", id) - _, secret := getFirstObject(objects, "Secret", id) + _, role := getFirstObject(objects, "ClusterRole", id) if _, err = manager.ApplyAllStaged(ctx, objects, false, timeout); err != nil { t.Fatal(err) } t.Run("deletes objects in order", func(t *testing.T) { - changeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{binding, configMap}) + changeSet, err := manager.DeleteAll(ctx, objects) if err != nil { t.Fatal(err) } // expected deleted order var expected []string - for _, object := range []*unstructured.Unstructured{configMap, binding} { + for _, object := range objects { expected = append(expected, objectutil.FmtUnstructured(object)) } @@ -64,28 +62,24 @@ func TestDelete(t *testing.T) { t.Fatal(err) } - bindingClone := binding.DeepCopy() - err = manager.client.Get(ctx, client.ObjectKeyFromObject(bindingClone), bindingClone) + roleClone := role.DeepCopy() + err = manager.client.Get(ctx, client.ObjectKeyFromObject(roleClone), roleClone) if !apierrors.IsNotFound(err) { t.Fatal(err) } }) t.Run("waits for objects termination", func(t *testing.T) { - set := []*unstructured.Unstructured{role, secret} - _, err := manager.DeleteAll(ctx, set) + _, err := manager.DeleteAll(ctx, objects) if err != nil { t.Fatal(err) } - if err := manager.WaitForTermination(set, time.Second, 5*time.Second); err != nil { - t.Fatal(err) - } - - secretClone := secret.DeepCopy() - err = manager.client.Get(ctx, client.ObjectKeyFromObject(secretClone), secretClone) - if !apierrors.IsNotFound(err) { - t.Fatal(err) + if err := manager.WaitForTermination(objects, time.Second, 5*time.Second); err != nil { + // workaround for https://github.com/kubernetes-sigs/controller-runtime/issues/880 + if !strings.Contains(err.Error(), "Namespace/") { + t.Fatal(err) + } } }) } diff --git a/pkg/manager/manager_diff.go b/pkg/manager/manager_diff.go index 43d06bc..43c6408 100644 --- a/pkg/manager/manager_diff.go +++ b/pkg/manager/manager_diff.go @@ -20,12 +20,13 @@ package manager import ( "context" "fmt" - "github.com/stefanprodan/kustomizer/pkg/objectutil" "strings" "github.com/google/go-cmp/cmp" + "github.com/stefanprodan/kustomizer/pkg/objectutil" apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -114,14 +115,22 @@ func (m *ResourceManager) validationError(object *unstructured.Unstructured, err return fmt.Errorf("%s namespace not specified, error: %w", objectutil.FmtUnstructured(object), err) } + reason := fmt.Sprintf("%v", apierrors.ReasonForError(err)) + if object.GetKind() == "Secret" { msg := "data values must be of type string" if strings.Contains(err.Error(), "immutable") { msg = "secret is immutable" } - return fmt.Errorf("%s is invalid, error: %s", objectutil.FmtUnstructured(object), msg) + return fmt.Errorf("%s %s, error: %s", objectutil.FmtUnstructured(object), strings.ToLower(reason), msg) + } + + // detect managed field conflict + if status, ok := apierrors.StatusCause(err, metav1.CauseTypeFieldManagerConflict); ok { + reason = fmt.Sprintf("%v", status.Type) } - return fmt.Errorf("%s is invalid, error: %w", objectutil.FmtUnstructured(object), err) + return fmt.Errorf("%s dry-run falied, reason: %s, error: %w", + objectutil.FmtUnstructured(object), reason, err) } diff --git a/pkg/manager/manager_wait.go b/pkg/manager/manager_wait.go index c21bf3a..324eb0c 100644 --- a/pkg/manager/manager_wait.go +++ b/pkg/manager/manager_wait.go @@ -20,10 +20,11 @@ package manager import ( "context" "fmt" - "github.com/stefanprodan/kustomizer/pkg/objectutil" "strings" "time" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/wait" @@ -109,7 +110,7 @@ func (m *ResourceManager) WaitForTermination(objects []*unstructured.Unstructure for _, object := range objects { if err := wait.PollImmediate(interval, timeout, m.isDeleted(ctx, object)); err != nil { - return err + return fmt.Errorf("%s termination timeout, error: %w", objectutil.FmtUnstructured(object), err) } } return nil diff --git a/pkg/objectutil/io.go b/pkg/objectutil/io.go index 2cb6ee9..e655ef2 100644 --- a/pkg/objectutil/io.go +++ b/pkg/objectutil/io.go @@ -68,12 +68,21 @@ func ReadObjects(r io.Reader) ([]*unstructured.Unstructured, error) { continue } - objects = append(objects, obj) + if IsKubernetesObject(obj) { + objects = append(objects, obj) + } } return objects, nil } +func IsKubernetesObject(object *unstructured.Unstructured) bool { + if object.GetName() == "" || object.GetKind() == "" || object.GetAPIVersion() == "" { + return false + } + return true +} + // ObjectsToYAML encodes the given Kubernetes API objects to a YAML multi-doc. func ObjectsToYAML(objects []*unstructured.Unstructured) (string, error) { var builder strings.Builder diff --git a/pkg/objectutil/sort.go b/pkg/objectutil/sort.go deleted file mode 100644 index 5ea0de7..0000000 --- a/pkg/objectutil/sort.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2021 Stefan Prodan -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 objectutil - -import ( - "strings" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -// ApplyOrder implements the Sort interface for Kubernetes objects based on kind. -// When creating objects: CRDs, namespaces and other global kinds go first, while webhooks go last. -// When deleting objects: the order should be inverted to allow the Kubernetes controllers to finalize custom resources. -type ApplyOrder []*unstructured.Unstructured - -func (objects ApplyOrder) Len() int { - return len(objects) -} - -func (objects ApplyOrder) Swap(i, j int) { - objects[i], objects[j] = objects[j], objects[i] -} - -func (objects ApplyOrder) Less(i, j int) bool { - ki := objects[i].GetKind() - ni := objects[i].GetName() - kj := objects[j].GetKind() - nj := objects[j].GetName() - ranki, rankj := RankOfKind(ki), RankOfKind(kj) - if ranki == rankj { - return ni < nj - } - return ranki < rankj -} - -// RankOfKind returns an int denoting the position of the given kind in the partial ordering of Kubernetes resources. -func RankOfKind(kind string) int { - switch strings.ToLower(kind) { - case "customresourcedefinition": - return 0 - case "namespace": - return 1 - case "apiservice", "clusterrole", "clusterrolebinding", "ingressclass", "runtimeclass", "storageclass", "priorityclass", "certificatesigningrequest", "podsecuritypolicy": - return 2 - case "secret", "configmap", "lease", "serviceaccount", "role", "rolebinding", "service", "endpoint", "endpointslice", "ingress", "networkpolicy": - return 3 - case "resourcequota", "limitrange", "podpreset", "persistentvolume", "persistentvolumeclaim", "poddisruptionbudget", "horizontalpodautoscaler": - return 4 - case "daemonset", "deployment", "job", "cronjob", "statefulset", "replicationcontroller", "replicaset", "pod": - return 5 - default: - return 6 - case "mutatingwebhookconfiguration", "validatingwebhookconfiguration": - return 7 - } -} - -func IsClusterDefinition(kind string) bool { - return RankOfKind(kind) < 2 -} From ec3485ed1a8fd9a8eded451857ca27f5ec822d11 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 6 Sep 2021 11:12:49 +0300 Subject: [PATCH 36/40] Do apply in stages and fix replicas conflicts Signed-off-by: Stefan Prodan --- .github/workflows/e2e.yaml | 6 +- Makefile | 2 +- cmd/kustomizer/apply.go | 71 +++++++++----- cmd/kustomizer/build.go | 14 ++- cmd/kustomizer/delete.go | 8 +- pkg/inventory/inventory.go | 9 +- pkg/manager/manager_apply.go | 9 +- pkg/manager/manager_apply_test.go | 35 +------ pkg/manager/manager_delete.go | 4 +- pkg/manager/manager_inventory.go | 2 +- pkg/objectutil/io.go | 22 ++++- pkg/objectutil/sort.go | 123 +++++++++++++++++++++++++ testdata/plain/backend/deployment.yaml | 1 + testdata/plain/backend/hpa.yaml | 4 +- 14 files changed, 229 insertions(+), 81 deletions(-) create mode 100644 pkg/objectutil/sort.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index cec7ba5..c9caaae 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -67,13 +67,13 @@ jobs: kubectl get crd tests.testing.kustomizer.dev 2>&1 | grep NotFound - name: Load test apply (110 objects) run: | - ./bin/kustomizer apply -k ./testdata/loadtest/ --inventory-name load-test + ./bin/kustomizer apply -i load-test -k ./testdata/loadtest/ - name: Load test delete (110 objects) run: | - ./bin/kustomizer delete --inventory-name load-test + ./bin/kustomizer delete -i load-test - name: Test staged apply run: | - ./bin/kustomizer apply -k ./testdata/certs/ --prune --wait --inventory-name cert-test --mode=ApplyAllStaged + ./bin/kustomizer apply -i cert-test -k ./testdata/certs/ --prune --wait kubectl -n kustomizer-cert-test wait issuers/my-ca-issuer --for=condition=ready --timeout=1m - name: Debug failure if: failure() diff --git a/Makefile b/Makefile index ad37944..9442ab6 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ build: CGO_ENABLED=0 go build -o ./bin/kustomizer ./cmd/kustomizer install: - go install cmd/kustomizer + go install ./cmd/kustomizer install-dev: CGO_ENABLED=0 go build -o /usr/local/bin ./cmd/kustomizer diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index d579e35..5ca91ad 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -19,11 +19,15 @@ package main import ( "context" "fmt" + "sort" "time" - "github.com/spf13/cobra" "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) var applyCmd = &cobra.Command{ @@ -40,7 +44,6 @@ type applyFlags struct { wait bool force bool prune bool - mode string } var applyArgs applyFlags @@ -56,8 +59,7 @@ func init() { applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "The namespace of the inventory configmap. The namespace must exist on the target cluster.") - applyCmd.Flags().StringVar(&applyArgs.mode, "mode", "Apply", - "The ResourceManager apply method, can be `Apply`, `ApplyAll`, `ApplyAllStaged`.") + rootCmd.AddCommand(applyCmd) } @@ -84,6 +86,10 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { } logger.Println(fmt.Sprintf("applying %v manifest(s)...", len(objects))) + for _, object := range objects { + fixReplicasConflict(object, objects) + } + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { return fmt.Errorf("client init failed: %w", err) @@ -101,33 +107,41 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - switch applyArgs.mode { - case "Apply": - for _, object := range objects { - change, err := resMgr.Apply(ctx, object, applyArgs.force) - if err != nil { - return err - } - logger.Println(change.String()) + // contains only CRDs and Namespaces + var stageOne []*unstructured.Unstructured + + // contains all objects except for CRDs and Namespaces + var stageTwo []*unstructured.Unstructured + + for _, u := range objects { + if resMgr.IsClusterDefinition(u.GetKind()) { + stageOne = append(stageOne, u) + } else { + stageTwo = append(stageTwo, u) } - case "ApplyAll": - changeSet, err := resMgr.ApplyAll(ctx, objects, applyArgs.force) + } + + if len(stageOne) > 0 { + changeSet, err := resMgr.ApplyAll(ctx, stageOne, applyArgs.force) if err != nil { return err } for _, change := range changeSet.Entries { logger.Println(change.String()) } - case "ApplyAllStaged": - changeSet, err := resMgr.ApplyAllStaged(ctx, objects, applyArgs.force, 30*time.Second) - if err != nil { + + if err := resMgr.Wait(stageOne, 2*time.Second, 30*time.Second); err != nil { return err } - for _, change := range changeSet.Entries { - logger.Println(change.String()) + } + + sort.Sort(objectutil.SortableUnstructureds(stageTwo)) + for _, object := range stageTwo { + change, err := resMgr.Apply(ctx, object, applyArgs.force) + if err != nil { + return err } - default: - return fmt.Errorf("mode not supported") + logger.Println(change.String()) } staleObjects, err := resMgr.GetInventoryStaleObjects(ctx, newInventory) @@ -170,3 +184,18 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return nil } + +// fixReplicasConflict removes the replicas field from the given workload if it's managed by an HPA +func fixReplicasConflict(object *unstructured.Unstructured, objects []*unstructured.Unstructured) { + for _, hpa := range objects { + if hpa.GetKind() == "HorizontalPodAutoscaler" && object.GetNamespace() == hpa.GetNamespace() { + targetKind, found, err := unstructured.NestedFieldCopy(hpa.Object, "spec", "scaleTargetRef", "kind") + if err == nil && found && fmt.Sprintf("%v", targetKind) == object.GetKind() { + targetName, found, err := unstructured.NestedFieldCopy(hpa.Object, "spec", "scaleTargetRef", "name") + if err == nil && found && fmt.Sprintf("%v", targetName) == object.GetName() { + unstructured.RemoveNestedField(object.Object, "spec", "replicas") + } + } + } + } +} diff --git a/cmd/kustomizer/build.go b/cmd/kustomizer/build.go index 2394093..d2e5ec4 100644 --- a/cmd/kustomizer/build.go +++ b/cmd/kustomizer/build.go @@ -26,14 +26,13 @@ import ( "sort" "sync" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/kustomize/api/krusty" kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filesys" - - "github.com/stefanprodan/kustomizer/pkg/objectutil" ) var buildCmd = &cobra.Command{ @@ -122,11 +121,16 @@ func buildManifests(kustomizePath string, filePaths []string) ([]*unstructured.U if err != nil { return nil, fmt.Errorf("%s: %w", manifest, err) } - objects = append(objects, objs...) + + for _, obj := range objs { + if objectutil.IsKubernetesObject(obj) && !objectutil.IsKustomization(obj) { + objects = append(objects, obj) + } + } } } - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) return objects, nil } diff --git a/cmd/kustomizer/delete.go b/cmd/kustomizer/delete.go index e2fdff9..e423068 100644 --- a/cmd/kustomizer/delete.go +++ b/cmd/kustomizer/delete.go @@ -23,11 +23,11 @@ import ( "sort" "time" - "github.com/spf13/cobra" - "sigs.k8s.io/cli-utils/pkg/ordering" - "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + + "github.com/spf13/cobra" ) var deleteCmd = &cobra.Command{ @@ -89,7 +89,7 @@ func deleteCmdRun(cmd *cobra.Command, args []string) error { logger.Println(fmt.Sprintf("deleting %v manifest(s)...", len(objects))) hasErrors := false - sort.Sort(sort.Reverse(ordering.SortableUnstructureds(objects))) + sort.Sort(sort.Reverse(objectutil.SortableUnstructureds(objects))) for _, object := range objects { change, err := resMgr.Delete(ctx, object) if err != nil { diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index eed3869..8aabc99 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -20,10 +20,11 @@ package inventory import ( "sort" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/object" - "sigs.k8s.io/cli-utils/pkg/ordering" ) // Inventory is a record of objects that are applied on a cluster stored as a configmap. @@ -59,7 +60,7 @@ func NewInventory(name, namespace string) *Inventory { // AddObjects extracts the metadata from the given objects and adds it to the inventory. func (inv *Inventory) AddObjects(objects []*unstructured.Unstructured) error { - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) for _, om := range objects { objMetadata := object.UnstructuredToObjMeta(om) gv, err := schema.ParseGroupVersion(om.GetAPIVersion()) @@ -107,7 +108,7 @@ func (inv *Inventory) ListObjects() ([]*unstructured.Unstructured, error) { objects = append(objects, u) } - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) return objects, nil } @@ -155,6 +156,6 @@ func (inv *Inventory) Diff(target *Inventory) ([]*unstructured.Unstructured, err objects = append(objects, u) } - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) return objects, nil } diff --git a/pkg/manager/manager_apply.go b/pkg/manager/manager_apply.go index 4bf24a2..a269e94 100644 --- a/pkg/manager/manager_apply.go +++ b/pkg/manager/manager_apply.go @@ -25,7 +25,6 @@ import ( "time" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/stefanprodan/kustomizer/pkg/objectutil" @@ -71,7 +70,7 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru // ApplyAll performs a server-side dry-run of the given objects, and based on the diff result, // it applies the objects that are new or modified. func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.Unstructured, force bool) (*ChangeSet, error) { - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) changeSet := NewChangeSet() var toApply []*unstructured.Unstructured for _, object := range objects { @@ -123,11 +122,11 @@ func (m *ResourceManager) ApplyAllStaged(ctx context.Context, objects []*unstruc // contains only CRDs and Namespaces var stageOne []*unstructured.Unstructured - // contains all objects except for CRDs and Namespaces + // contains all objects except for CRDs and Namespaces var stageTwo []*unstructured.Unstructured for _, u := range objects { - if m.isClusterDefinition(u.GetKind()) { + if m.IsClusterDefinition(u.GetKind()) { stageOne = append(stageOne, u) } else { stageTwo = append(stageTwo, u) @@ -172,7 +171,7 @@ func (m *ResourceManager) apply(ctx context.Context, object *unstructured.Unstru return m.client.Patch(ctx, object, client.Apply, opts...) } -func (m *ResourceManager) isClusterDefinition(kind string) bool { +func (m *ResourceManager) IsClusterDefinition(kind string) bool { switch strings.ToLower(kind) { case "customresourcedefinition": case "namespace": diff --git a/pkg/manager/manager_apply_test.go b/pkg/manager/manager_apply_test.go index cc38948..fbf434b 100644 --- a/pkg/manager/manager_apply_test.go +++ b/pkg/manager/manager_apply_test.go @@ -8,10 +8,10 @@ import ( "testing" "time" - "github.com/google/go-cmp/cmp" "github.com/stefanprodan/kustomizer/pkg/objectutil" + + "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -39,7 +39,7 @@ func TestApply(t *testing.T) { } // expected created order - sort.Sort(ordering.SortableUnstructureds(objects)) + sort.Sort(objectutil.SortableUnstructureds(objects)) var expected []string for _, object := range objects { expected = append(expected, objectutil.FmtUnstructured(object)) @@ -122,35 +122,6 @@ func TestApply(t *testing.T) { } }) - t.Run("recreates deleted objects", func(t *testing.T) { - // delete the configmap - deletedChangeSet, err := manager.DeleteAll(ctx, []*unstructured.Unstructured{configMap}) - for _, entry := range deletedChangeSet.Entries { - if diff := cmp.Diff(string(DeletedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } - - // reapply objects - changeSet, err := manager.ApplyAllStaged(ctx, objects, false, timeout) - if err != nil { - t.Fatal(err) - } - - // verify the configmap was recreated - for _, entry := range changeSet.Entries { - if entry.Subject == configMapName { - if diff := cmp.Diff(string(CreatedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } else { - if diff := cmp.Diff(string(UnchangedAction), entry.Action); diff != "" { - t.Errorf("Mismatch from expected value (-want +got):\n%s", diff) - } - } - } - }) - t.Run("fails to apply immutable secret", func(t *testing.T) { // update a value in the secret err = unstructured.SetNestedField(secret.Object, "val-secret", "stringData", "key") diff --git a/pkg/manager/manager_delete.go b/pkg/manager/manager_delete.go index 4d567a3..82f4785 100644 --- a/pkg/manager/manager_delete.go +++ b/pkg/manager/manager_delete.go @@ -23,9 +23,9 @@ import ( "sort" "github.com/stefanprodan/kustomizer/pkg/objectutil" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/cli-utils/pkg/ordering" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -48,7 +48,7 @@ func (m *ResourceManager) Delete(ctx context.Context, object *unstructured.Unstr // DeleteAll deletes the given set of objects (not found errors are ignored). func (m *ResourceManager) DeleteAll(ctx context.Context, objects []*unstructured.Unstructured) (*ChangeSet, error) { - sort.Sort(sort.Reverse(ordering.SortableUnstructureds(objects))) + sort.Sort(sort.Reverse(objectutil.SortableUnstructureds(objects))) changeSet := NewChangeSet() for _, object := range objects { diff --git a/pkg/manager/manager_inventory.go b/pkg/manager/manager_inventory.go index c700e52..ae46b62 100644 --- a/pkg/manager/manager_inventory.go +++ b/pkg/manager/manager_inventory.go @@ -90,7 +90,7 @@ func (m *ResourceManager) DeleteInventory(ctx context.Context, i *inventory.Inve return nil } -// GetInventoryStaleObjects returns the list of objects subject to pruning. +// GetInventoryStaleObjects returns the list of objects metadata subject to pruning. func (m *ResourceManager) GetInventoryStaleObjects(ctx context.Context, i *inventory.Inventory) ([]*unstructured.Unstructured, error) { objects := make([]*unstructured.Unstructured, 0) existingInventory := inventory.NewInventory(i.Name, i.Namespace) diff --git a/pkg/objectutil/io.go b/pkg/objectutil/io.go index e655ef2..a9e19b2 100644 --- a/pkg/objectutil/io.go +++ b/pkg/objectutil/io.go @@ -68,7 +68,7 @@ func ReadObjects(r io.Reader) ([]*unstructured.Unstructured, error) { continue } - if IsKubernetesObject(obj) { + if IsKubernetesObject(obj) && !IsKustomization(obj) { objects = append(objects, obj) } } @@ -83,6 +83,26 @@ func IsKubernetesObject(object *unstructured.Unstructured) bool { return true } +func IsKustomization(object *unstructured.Unstructured) bool { + if object.GetKind() == "Kustomization" && object.GroupVersionKind().GroupKind().Group == "kustomize.config.k8s.io" { + return true + } + return false +} + +// ObjectToYAML encodes the given Kubernetes API object to YAML. +func ObjectToYAML(object *unstructured.Unstructured) string { + var builder strings.Builder + data, err := yaml.Marshal(object) + if err != nil { + return "" + } + builder.Write(data) + builder.WriteString("---\n") + + return builder.String() +} + // ObjectsToYAML encodes the given Kubernetes API objects to a YAML multi-doc. func ObjectsToYAML(objects []*unstructured.Unstructured) (string, error) { var builder strings.Builder diff --git a/pkg/objectutil/sort.go b/pkg/objectutil/sort.go new file mode 100644 index 0000000..da33d2f --- /dev/null +++ b/pkg/objectutil/sort.go @@ -0,0 +1,123 @@ +/* +Copyright 2021 Stefan Prodan. +Copyright 2020 The Kubernetes 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 objectutil + +import ( + "sort" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/object" +) + +type SortableUnstructureds []*unstructured.Unstructured + +var _ sort.Interface = SortableUnstructureds{} + +func (a SortableUnstructureds) Len() int { return len(a) } +func (a SortableUnstructureds) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortableUnstructureds) Less(i, j int) bool { + first := object.UnstructuredToObjMeta(a[i]) + second := object.UnstructuredToObjMeta(a[j]) + return less(first, second) +} + +type SortableMetas []object.ObjMetadata + +var _ sort.Interface = SortableMetas{} + +func (a SortableMetas) Len() int { return len(a) } +func (a SortableMetas) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortableMetas) Less(i, j int) bool { + return less(a[i], a[j]) +} + +func less(i, j object.ObjMetadata) bool { + if !Equals(i.GroupKind, j.GroupKind) { + return IsLessThan(i.GroupKind, j.GroupKind) + } + // In case of tie, compare the namespace and name combination so that the output + // order is consistent irrespective of input order + if i.Namespace != j.Namespace { + return i.Namespace < j.Namespace + } + return i.Name < j.Name +} + +var kind2index = computeKind2index() + +func computeKind2index() map[string]int { + // An attempt to order things to help k8s, e.g. + // a Service should come before things that refer to it. + // Namespace should be first. + // In some cases order just specified to provide determinism. + orderFirst := []string{ + "CustomResourceDefinition", + "Namespace", + "ResourceQuota", + "StorageClass", + "ServiceAccount", + "PodSecurityPolicy", + "Role", + "ClusterRole", + "RoleBinding", + "ClusterRoleBinding", + "ConfigMap", + "Secret", + "Service", + "LimitRange", + "PriorityClass", + "Deployment", + "StatefulSet", + "CronJob", + "PodDisruptionBudget", + } + orderLast := []string{ + "MutatingWebhookConfiguration", + "ValidatingWebhookConfiguration", + } + kind2indexResult := make(map[string]int, len(orderFirst)+len(orderLast)) + for i, n := range orderFirst { + kind2indexResult[n] = -len(orderFirst) + i + } + for i, n := range orderLast { + kind2indexResult[n] = 1 + i + } + return kind2indexResult +} + +// getIndexByKind returns the index of the kind respecting the order +func getIndexByKind(kind string) int { + return kind2index[kind] +} + +func Equals(i, j schema.GroupKind) bool { + return i.Group == j.Group && i.Kind == j.Kind +} + +func IsLessThan(i, j schema.GroupKind) bool { + indexI := getIndexByKind(i.Kind) + indexJ := getIndexByKind(j.Kind) + if indexI != indexJ { + return indexI < indexJ + } + if i.Group != j.Group { + return i.Group < j.Group + } + return i.Kind < j.Kind +} diff --git a/testdata/plain/backend/deployment.yaml b/testdata/plain/backend/deployment.yaml index 3ef54f6..bcc723c 100644 --- a/testdata/plain/backend/deployment.yaml +++ b/testdata/plain/backend/deployment.yaml @@ -4,6 +4,7 @@ metadata: name: backend namespace: kustomizer-demo spec: + replicas: 1 minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 diff --git a/testdata/plain/backend/hpa.yaml b/testdata/plain/backend/hpa.yaml index 7359fde..edd00aa 100644 --- a/testdata/plain/backend/hpa.yaml +++ b/testdata/plain/backend/hpa.yaml @@ -8,8 +8,8 @@ spec: apiVersion: apps/v1 kind: Deployment name: backend - minReplicas: 1 - maxReplicas: 2 + minReplicas: 2 + maxReplicas: 4 metrics: - type: Resource resource: From ddd10a79dccfbff409760d197352b9bad75cc538 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 6 Sep 2021 14:37:47 +0300 Subject: [PATCH 37/40] Update readme to v1 Signed-off-by: Stefan Prodan --- README.md | 151 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index a2f7ca6..3cd34df 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # kustomizer [![e2e](https://github.com/stefanprodan/kustomizer/workflows/e2e/badge.svg)](https://github.com/stefanprodan/kustomizer/actions) -[![license](https://img.shields.io/github/license/stefanprodan/kustomizer.svg)](https://github.com/stefanprodan/kustomizer/blob/master/LICENSE) +[![license](https://img.shields.io/github/license/stefanprodan/kustomizer.svg)](https://github.com/stefanprodan/kustomizer/blob/main/LICENSE) [![release](https://img.shields.io/github/release/stefanprodan/kustomizer/all.svg)](https://github.com/stefanprodan/kustomizer/releases) -Kustomizer is a command-line utility for applying kustomizations on Kubernetes clusters. +Kustomizer is a command-line utility for reconciling Kubernetes manifests and Kustomize overlays onto clusters. Kustomizer garbage collector keeps track of the applied resources and prunes the Kubernetes -objects that were previously applied on the cluster but are missing from the current revision. +objects that were previously applied but are missing from the current inventory. ## Install @@ -20,85 +20,102 @@ curl -s https://kustomizer.dev/install/kustomizer.sh | sudo bash Windows users can download the binary from the [release page](https://github.com/stefanprodan/kustomizer/releases). -If you want to use kustomizer as a kubectl plugin, rename the binary to `kubectl-kustomizer`: +## Available Commands -```bash -mv /usr/local/bin/kustomizer /usr/local/bin/kubectl-kustomizer -``` +The Kustomize CLI comes with the following commands: + +* `apply` Apply validates the given Kubernetes manifests or Kustomize overlays and reconciles them using server-side apply. +* `build` Build scans the given path for Kubernetes manifests or Kustomize overlays and prints the YAML multi-doc to stdout. +* `delete` Delete removes the Kubernetes objects in the inventory from the cluster and waits for termination. +* `diff` Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff to stdout. -## Usage +## Get Started -Apply a kustomization by pointing Kustomizer to a local dir that contains Kubernetes manifests: +Clone the Kustomizer Git repository locally: ```bash git clone https://github.com/stefanprodan/kustomizer cd kustomizer +``` + +Apply a local directory that contains Kubernetes manifests: -kustomizer apply testdata/plain --name=demo --revision=1.0.0 +```bash +kustomizer apply -f testdata/plain --prune --wait \ +--inventory-name=demo \ +--inventory-namespace=default ``` -Kustomizer generates a `kustomization.yaml` if one doesn't exist, builds it and applies the -resulting manifests on the cluster. +Kustomizer scans the given path recursively for Kubernetes manifests in YAML format, +validates them against the cluster, applies them with server-side apply, and finally +waits for the workloads to be rollout: ```text -$ kustomizer apply testdata/plain/ --name=demo --revision=1.0.0 - -namespace/kustomizer-demo created -serviceaccount/demo created -clusterrole.rbac.authorization.k8s.io/demo-read-only created -clusterrolebinding.rbac.authorization.k8s.io/demo-read-only created -service/backend created -service/frontend created -deployment.apps/backend created -deployment.apps/frontend created -horizontalpodautoscaler.autoscaling/backend created -horizontalpodautoscaler.autoscaling/frontend created -configmap/demo-snapshot created +building inventory... +applying 10 manifest(s)... +Namespace/kustomizer-demo created +ServiceAccount/kustomizer-demo/demo created +ClusterRole/kustomizer-demo-read-only created +ClusterRoleBinding/kustomizer-demo-read-only created +Service/kustomizer-demo/backend created +Service/kustomizer-demo/frontend created +Deployment/kustomizer-demo/backend created +Deployment/kustomizer-demo/frontend created +HorizontalPodAutoscaler/kustomizer-demo/backend created +HorizontalPodAutoscaler/kustomizer-demo/frontend created +waiting for resources to become ready... +all resources are ready ``` -After applying the resources, Kustomizer creates a ConfigMap in the format `-snapshot` -used for garbage collection. You can change the ConfigMap namespace with `--gc-namespace` arg. +After applying the resources, Kustomizer creates a ConfigMap used for garbage collection. -Remove the `frontend` and `rbac` manifests from the local dir: +Remove the `frontend` and the `rbac` manifests from the local dir: ```bash rm -rf testdata/plain/frontend rm -rf testdata/plain/common/rbac.yaml ``` -Rerun the apply by changing the revision: - -```text -$ kustomizer apply testdata/plain/ --name=demo --revision=2.0.0 - -namespace/kustomizer-demo configured -serviceaccount/demo configured -service/backend configured -deployment.apps/backend configured -horizontalpodautoscaler.autoscaling/backend configured -deployment.apps "frontend" deleted -horizontalpodautoscaler.autoscaling "frontend" deleted -service "frontend" deleted -clusterrole.rbac.authorization.k8s.io "demo-read-only" deleted -clusterrolebinding.rbac.authorization.k8s.io "demo-read-only" deleted -configmap/demo-snapshot configured +Rerun the apply command: + +```console +$ kustomizer apply -i demo -f testdata/plain/ --prune --wait + +building inventory... +applying 5 manifest(s)... +Namespace/kustomizer-demo unchanged +ServiceAccount/kustomizer-demo/demo unchanged +Service/kustomizer-demo/backend unchanged +Deployment/kustomizer-demo/backend unchanged +HorizontalPodAutoscaler/kustomizer-demo/backend unchanged +HorizontalPodAutoscaler/kustomizer-demo/frontend deleted +Deployment/kustomizer-demo/frontend deleted +Service/kustomizer-demo/frontend deleted +ClusterRoleBinding/kustomizer-demo-read-only deleted +ClusterRole/kustomizer-demo-read-only deleted +waiting for resources to become ready... +all resources are ready ``` -After applying the resources, Kustomizer removes the Kubernetes objects that are not present in the current revision. +After applying the resources, Kustomizer removes the Kubernetes objects that are not present in the current inventory. Kustomizer garbage collector deletes the namespaced objects first then it removes the non-namspaced ones. -After the garbage collection finishes, Kustomizer update the ConfigMap snapshot with the new revision number. - -Delete all the Kubernetes objects belonging to a kustomization including the ConfigMap snapshot: - -```text -$ kustomizer delete --name=demo - -deployment.apps "backend" deleted -horizontalpodautoscaler.autoscaling "backend" deleted -service "backend" deleted -serviceaccount "demo" deleted -namespace "kustomizer-demo" deleted -configmap "demo-snapshot" deleted +After the garbage collection finishes, Kustomizer update the ConfigMap inventory with the latest entries. + +Delete all the Kubernetes objects belonging to an inventory including the inventory ConfigMap: + +```console +$ kustomizer delete -i demo --wait + +retrieving inventory... +deleting 5 manifest(s)... +HorizontalPodAutoscaler/kustomizer-demo/backend deleted +Deployment/kustomizer-demo/backend deleted +Service/kustomizer-demo/backend deleted +ServiceAccount/kustomizer-demo/demo deleted +Namespace/kustomizer-demo deleted +ConfigMap/default/demo deleted +waiting for resources to be terminated... +all resources have been deleted ``` ## CIOps @@ -112,7 +129,7 @@ name: deploy on: push: branches: - - 'master' + - 'main' jobs: kustomizer: @@ -124,12 +141,13 @@ jobs: with: kubeconfig: ${{ secrets.KUBE_CONFIG }} - name: Install Kustomizer - uses: stefanprodan/kustomizer/action@master - - name: Apply changes - run: kustomizer apply testdata/plain/ --name=demo --revision=${GITHUB_SHA} + uses: stefanprodan/kustomizer/action@main + - name: Deploy + run: kustomizer apply -f testdata/plain/ -i my-app --wait --prune ``` -For running kustomizations in a **GitOps** manner, take a look at [kustomize-controller](https://github.com/fluxcd/kustomize-controller). +For deploying to Kubernetes in a **GitOps** manner, +take a look at [Flux](https://github.com/fluxcd/flux2). ## Motivation @@ -146,11 +164,8 @@ Another downside is the fact that pruning can delete non-namespaced objects outs If you want to prune custom resources, then you need to pass the group/version/kind to prune-whitelist and maintain a list per kustomization. -Kustomizer takes the supplied name and revision, and using Kustomize transformers, it labels all -the Kubernetes objects before applying them on the cluster. -The name, revision and objects metadata are persisted on the cluster in a ConfigMap. -When the revision changes, Kustomizer can reliably detect the objects that were previously applied but -are missing from the current revision. For namespaced objects, Kustomizer runs the delete commands +Kustomizer can reliably detect the objects that were previously applied but +are missing from the current inventory. For namespaced objects, Kustomizer runs the delete commands scoped to a namespace, this way an account that doesn't have a cluster role binding can prune objects in the namespaces it owns. From e44c2101a8bcf21dff7a140dfc7c5a5fac2cf957 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 6 Sep 2021 17:31:26 +0300 Subject: [PATCH 38/40] Update install instructions Signed-off-by: Stefan Prodan --- .goreleaser.yml | 14 -- Formula/kustomizer.rb | 34 ---- README.md | 11 +- action/action.yml | 6 +- install/README.md | 12 +- install/kustomizer.sh | 223 +++++++++++++++++++++---- testdata/plain/backend/deployment.yaml | 1 - 7 files changed, 201 insertions(+), 100 deletions(-) delete mode 100644 Formula/kustomizer.rb diff --git a/.goreleaser.yml b/.goreleaser.yml index e98630c..440e56e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -16,17 +16,3 @@ archives: - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" files: - LICENSE -brews: - - tap: - owner: stefanprodan - name: kustomizer - folder: Formula - homepage: "https://kustomizer.dev/" - description: "Kustomize build, apply, prune command-line utility." - - dependencies: - - name: kubectl - type: optional - - test: | - system "#{bin}/kustomizer --version" \ No newline at end of file diff --git a/Formula/kustomizer.rb b/Formula/kustomizer.rb deleted file mode 100644 index f1698f0..0000000 --- a/Formula/kustomizer.rb +++ /dev/null @@ -1,34 +0,0 @@ -# This file was generated by GoReleaser. DO NOT EDIT. -class Kustomizer < Formula - desc "Kustomize build, apply, prune command-line utility." - homepage "https://kustomizer.dev/" - version "0.2.1" - bottle :unneeded - - if OS.mac? - url "https://github.com/stefanprodan/kustomizer/releases/download/v0.2.1/kustomizer_0.2.1_darwin_amd64.tar.gz" - sha256 "afb30c0b4caae50d7e8c671990fd7150a0fb4a160f90d16c760a268b092fe300" - elsif OS.linux? - if Hardware::CPU.intel? - url "https://github.com/stefanprodan/kustomizer/releases/download/v0.2.1/kustomizer_0.2.1_linux_amd64.tar.gz" - sha256 "0dcf3a2976fadf5961fc2ca4df914a207d4a483def1ee892d03d41450e1ea345" - end - if Hardware::CPU.arm? - if Hardware::CPU.is_64_bit? - url "https://github.com/stefanprodan/kustomizer/releases/download/v0.2.1/kustomizer_0.2.1_linux_arm64.tar.gz" - sha256 "ce0e546e0f431425587b9630d31fb2735e953981e468b204d6e685b28b93a518" - else - end - end - end - - depends_on "kubectl" => :optional - - def install - bin.install "kustomizer" - end - - test do - system "#{bin}/kustomizer --version" - end -end diff --git a/README.md b/README.md index 3cd34df..7782dcb 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,15 @@ objects that were previously applied but are missing from the current inventory. ## Install -Download the Kustomizer binary from the -[release page](https://github.com/stefanprodan/kustomizer/releases) -or run [this script](install/README.md): +The Kustomizer CLI is available as a binary executable for all major platforms, +the binaries can be downloaded form GitHub [release page](https://github.com/stefanprodan/kustomizer/releases). + +Install the latest release on macOS or Linux with [this script](install/README.md): ```bash -curl -s https://kustomizer.dev/install/kustomizer.sh | sudo bash +curl -s https://kustomizer.dev/install.sh | bash ``` -Windows users can download the binary from the [release page](https://github.com/stefanprodan/kustomizer/releases). - ## Available Commands The Kustomize CLI comes with the following commands: diff --git a/action/action.yml b/action/action.yml index 31ce05f..613abd8 100644 --- a/action/action.yml +++ b/action/action.yml @@ -1,12 +1,12 @@ name: Setup Kustomizer CLI -description: A GitHub Action for running kustomizer commands +description: A GitHub Action for running Kustomizer commands author: Stefan Prodan branding: color: blue icon: command inputs: version: - description: "Kustomizer version e.g. 0.8.0 (defaults to latest stable release)" + description: "Kustomizer version e.g. 1.0.0 (defaults to latest stable release)" required: false arch: description: "arch can be amd64, arm64 or arm" @@ -15,7 +15,7 @@ inputs: runs: using: composite steps: - - name: "Download Kustomizer binary to tmp" + - name: "Download kustomizer binary to tmp" shell: bash run: | ARCH=${{ inputs.arch }} diff --git a/install/README.md b/install/README.md index 903877e..275228b 100644 --- a/install/README.md +++ b/install/README.md @@ -1,17 +1,15 @@ # Kustomizer Installation -Binaries for macOS, Linux and Windows AMD64 are available for download on the -[release page](https://github.com/stefanprodan/kustomizer/releases). - -To install the latest release run: +To install the latest release on macOS or Linux: ```bash -curl -s https://raw.githubusercontent.com/stefanprodan/kustomizer/master/install/kustomizer.sh | sudo bash +curl -s https://raw.githubusercontent.com/stefanprodan/kustomizer/main/install/kustomizer.sh | sudo bash ``` The install script does the following: * attempts to detect your OS -* downloads and unpacks the release tar file in a temporary directory +* downloads and unpacks the [release tar file](https://github.com/stefanprodan/kustomizer/releases) in a temporary directory +* verifies the binary checksum * copies the kustomizer binary to `/usr/local/bin` * removes the temporary directory @@ -24,7 +22,7 @@ git clone https://github.com/stefanprodan/kustomizer cd kustomizer ``` -Build the kustomizer binary (requires go >= 1.14): +Build the kustomizer binary (requires go >= 1.16): ```bash make build diff --git a/install/kustomizer.sh b/install/kustomizer.sh index 8574b5d..14133b2 100755 --- a/install/kustomizer.sh +++ b/install/kustomizer.sh @@ -1,51 +1,204 @@ #!/usr/bin/env bash +# Copyright 2021 Stefan Prodan +# Copyright 2020 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. + set -e DEFAULT_BIN_DIR="/usr/local/bin" -BIN_DIR=${1:-"$DEFAULT_BIN_DIR"} - -opsys="" -if [[ "$OSTYPE" == linux* ]]; then - opsys=linux -elif [[ "$OSTYPE" == darwin* ]]; then - opsys=darwin -fi - -if [[ "$opsys" == "" ]]; then - echo "OS $OSTYPE not supported" - exit 1 -fi - -if [[ ! -x "$(command -v curl)" ]]; then - echo "curl not found" +BIN_DIR=${1:-"${DEFAULT_BIN_DIR}"} +GITHUB_REPO="stefanprodan/kustomizer" + +# Helper functions for logs +info() { + echo '[INFO] ' "$@" +} + +warn() { + echo '[WARN] ' "$@" >&2 +} + +fatal() { + echo '[ERROR] ' "$@" >&2 exit 1 -fi +} + +# Set os, fatal if operating system not supported +setup_verify_os() { + if [[ -z "${OS}" ]]; then + OS=$(uname) + fi + case ${OS} in + Darwin) + OS=darwin + ;; + Linux) + OS=linux + ;; + *) + fatal "Unsupported operating system ${OS}" + esac +} + +# Set arch, fatal if architecture not supported +setup_verify_arch() { + if [[ -z "${ARCH}" ]]; then + ARCH=$(uname -m) + fi + case ${ARCH} in + arm64|aarch64|armv8l) + ARCH=arm64 + ;; + amd64) + ARCH=amd64 + ;; + x86_64) + ARCH=amd64 + ;; + *) + fatal "Unsupported architecture ${ARCH}" + esac +} + +# Verify existence of downloader executable +verify_downloader() { + # Return failure if it doesn't exist or is no executable + [[ -x "$(which "$1")" ]] || return 1 + + # Set verified executable as our downloader program and return success + DOWNLOADER=$1 + return 0 +} -tmpDir=`mktemp -d` -if [[ ! "$tmpDir" || ! -d "$tmpDir" ]]; then - echo "could not create temp dir" - exit 1 -fi +# Create tempory directory and cleanup when done +setup_tmp() { + TMP_DIR=$(mktemp -d -t kustomizer-install.XXXXXXXXXX) + TMP_METADATA="${TMP_DIR}/kustomizer.json" + TMP_HASH="${TMP_DIR}/kustomizer.hash" + TMP_BIN="${TMP_DIR}/kustomizer.tar.gz" + cleanup() { + local code=$? + set +e + trap - EXIT + rm -rf "${TMP_DIR}" + exit ${code} + } + trap cleanup INT EXIT +} + +# Find version from Github metadata +get_release_version() { + METADATA_URL="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" + + info "Downloading metadata ${METADATA_URL}" + download "${TMP_METADATA}" "${METADATA_URL}" + + VERSION_KUSTOMIZER=$(grep '"tag_name":' "${TMP_METADATA}" | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) + if [[ -n "${VERSION_KUSTOMIZER}" ]]; then + info "Using ${VERSION_KUSTOMIZER} as release" + else + fatal "Unable to determine release version" + fi +} + +# Download from file from URL +download() { + [[ $# -eq 2 ]] || fatal 'download needs exactly 2 arguments' + + case $DOWNLOADER in + curl) + curl -o "$1" -sfL "$2" + ;; + wget) + wget -qO "$1" "$2" + ;; + *) + fatal "Incorrect executable '${DOWNLOADER}'" + ;; + esac -function cleanup { - rm -rf "$tmpDir" + # Abort if download command failed + [[ $? -eq 0 ]] || fatal 'Download failed' } -trap cleanup EXIT +# Download hash from Github URL +download_hash() { + HASH_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION_KUSTOMIZER}/kustomizer_${VERSION_KUSTOMIZER}_checksums.txt" + set -e -pushd $tmpDir >& /dev/null + info "Downloading hash ${HASH_URL}" + download "${TMP_HASH}" "${HASH_URL}" + HASH_EXPECTED=$(grep " kustomizer_${VERSION_KUSTOMIZER}_${OS}_${ARCH}.tar.gz$" "${TMP_HASH}") + HASH_EXPECTED=${HASH_EXPECTED%%[[:blank:]]*} +} -curl -s https://api.github.com/repos/stefanprodan/kustomizer/releases/latest |\ - grep browser_download |\ - grep $opsys |\ - cut -d '"' -f 4 |\ - xargs curl -sL -o kustomizer.tar.gz +# Download binary from Github URL +download_binary() { + BIN_URL="https://github.com/${GITHUB_REPO}/releases/download/v${VERSION_KUSTOMIZER}/kustomizer_${VERSION_KUSTOMIZER}_${OS}_${ARCH}.tar.gz" + info "Downloading binary ${BIN_URL}" + download "${TMP_BIN}" "${BIN_URL}" +} -tar xzf ./kustomizer.tar.gz +compute_sha256sum() { + cmd=$(which sha256sum shasum | head -n 1) + case $(basename "$cmd") in + sha256sum) + sha256sum "$1" | cut -f 1 -d ' ' + ;; + shasum) + shasum -a 256 "$1" | cut -f 1 -d ' ' + ;; + *) + fatal "Can not find sha256sum or shasum to compute checksum" + ;; + esac +} -mv ./kustomizer $BIN_DIR +# Verify downloaded binary hash +verify_binary() { + info "Verifying binary download" + HASH_BIN=$(compute_sha256sum "${TMP_BIN}") + HASH_BIN=${HASH_BIN%%[[:blank:]]*} + if [[ "${HASH_EXPECTED}" != "${HASH_BIN}" ]]; then + fatal "Download sha256 does not match ${HASH_EXPECTED}, got ${HASH_BIN}" + fi +} -popd >& /dev/null +# Setup permissions and move binary +setup_binary() { + chmod 755 "${TMP_BIN}" + info "Installing kustomizer to ${BIN_DIR}/kustomizer" + tar -xzof "${TMP_BIN}" -C "${TMP_DIR}" -echo "$(kustomizer --version) installed" + local CMD_MOVE="mv -f \"${TMP_DIR}/kustomizer\" \"${BIN_DIR}\"" + if [[ -w "${BIN_DIR}" ]]; then + eval "${CMD_MOVE}" + else + eval "sudo ${CMD_MOVE}" + fi +} + +# Run the install process +{ + setup_verify_os + setup_verify_arch + verify_downloader curl || verify_downloader wget || fatal 'Can not find curl or wget for downloading files' + setup_tmp + get_release_version + download_hash + download_binary + verify_binary + setup_binary +} diff --git a/testdata/plain/backend/deployment.yaml b/testdata/plain/backend/deployment.yaml index bcc723c..3ef54f6 100644 --- a/testdata/plain/backend/deployment.yaml +++ b/testdata/plain/backend/deployment.yaml @@ -4,7 +4,6 @@ metadata: name: backend namespace: kustomizer-demo spec: - replicas: 1 minReadySeconds: 3 revisionHistoryLimit: 5 progressDeadlineSeconds: 60 From a9348df7a6a57ef826c9f86240a4ce966a5d99b9 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Mon, 6 Sep 2021 20:12:49 +0300 Subject: [PATCH 39/40] Implement get inventory command Signed-off-by: Stefan Prodan --- README.md | 60 +++++++++++++++---- cmd/kustomizer/apply.go | 5 ++ cmd/kustomizer/get.go | 37 ++++++++++++ cmd/kustomizer/get_inventories.go | 98 +++++++++++++++++++++++++++++++ cmd/kustomizer/get_inventory.go | 82 ++++++++++++++++++++++++++ pkg/inventory/inventory.go | 16 ++++- pkg/manager/doc.go | 9 ++- pkg/manager/manager_inventory.go | 17 ++++++ 8 files changed, 305 insertions(+), 19 deletions(-) create mode 100644 cmd/kustomizer/get.go create mode 100644 cmd/kustomizer/get_inventories.go create mode 100644 cmd/kustomizer/get_inventory.go diff --git a/README.md b/README.md index 7782dcb..1b252d4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ Kustomizer is a command-line utility for reconciling Kubernetes manifests and Kustomize overlays onto clusters. Kustomizer garbage collector keeps track of the applied resources and prunes the Kubernetes -objects that were previously applied but are missing from the current inventory. +objects that were previously applied but are missing from the current revision. + +Compared to `kubectl apply`, Kustomizer does things a little different: + +- Applies first custom resource definitions (CRDs) and namespaces, waits for them to register and only then applies the custom resources. +- Skips to apply resources that haven't changed + (determined with Kubernetes API [server-side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) dry-run). +- Waits for the applied resources to be fully reconciled (waits for replicasets rollout, ingress and other custom resources to become ready). +- Deletes stale objects like ConfigMap and Secrets generated with Kustomize or other tools. ## Install @@ -19,6 +27,8 @@ Install the latest release on macOS or Linux with [this script](install/README.m curl -s https://kustomizer.dev/install.sh | bash ``` +Kustomizer needs a Kubernetes cluster version **1.18** or later and a valid `kubeconfig` file. + ## Available Commands The Kustomize CLI comes with the following commands: @@ -27,6 +37,7 @@ The Kustomize CLI comes with the following commands: * `build` Build scans the given path for Kubernetes manifests or Kustomize overlays and prints the YAML multi-doc to stdout. * `delete` Delete removes the Kubernetes objects in the inventory from the cluster and waits for termination. * `diff` Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff to stdout. +* `get` Prints the content of inventories and their source revision. ## Get Started @@ -39,17 +50,13 @@ cd kustomizer Apply a local directory that contains Kubernetes manifests: -```bash -kustomizer apply -f testdata/plain --prune --wait \ +```console +$ kustomizer apply -f ./testdata/plain --prune --wait \ +--source="$(git ls-remote --get-url)" \ +--revision="$(git describe --always)" \ --inventory-name=demo \ --inventory-namespace=default -``` - -Kustomizer scans the given path recursively for Kubernetes manifests in YAML format, -validates them against the cluster, applies them with server-side apply, and finally -waits for the workloads to be rollout: -```text building inventory... applying 10 manifest(s)... Namespace/kustomizer-demo created @@ -66,7 +73,37 @@ waiting for resources to become ready... all resources are ready ``` -After applying the resources, Kustomizer creates a ConfigMap used for garbage collection. +Kustomizer scans the given path recursively for Kubernetes manifests in YAML format, +validates them against the cluster, applies them with server-side apply, and finally +waits for the workloads to be rollout. + +To apply Kustomize overlays, you can use `kustomizer apply -k path/to/overlay`, for more details see `--help`. + +After applying the resources, Kustomizer creates an inventory. +You can list the content of an inventory with: + +```console +$ kustomizer get inventory demo + +Inventory: default/demo +LastAppliedTime: 2021-09-06T16:33:08Z +Source: https://github.com/stefanprodan/kustomizer.git +Revision: e44c210 +Entries: +- Namespace/kustomizer-demo +- ServiceAccount/kustomizer-demo/demo +- ClusterRole/kustomizer-demo-read-only +- ClusterRoleBinding/kustomizer-demo-read-only +- Service/kustomizer-demo/backend +- Service/kustomizer-demo/frontend +- Deployment/kustomizer-demo/backend +- Deployment/kustomizer-demo/frontend +- HorizontalPodAutoscaler/kustomizer-demo/backend +- HorizontalPodAutoscaler/kustomizer-demo/frontend +``` + +The inventory entries are used to track which objects are subject to garbage collection. +The inventory is persistent on the cluster as a ConfigMap. Remove the `frontend` and the `rbac` manifests from the local dir: @@ -98,7 +135,6 @@ all resources are ready After applying the resources, Kustomizer removes the Kubernetes objects that are not present in the current inventory. Kustomizer garbage collector deletes the namespaced objects first then it removes the non-namspaced ones. -After the garbage collection finishes, Kustomizer update the ConfigMap inventory with the latest entries. Delete all the Kubernetes objects belonging to an inventory including the inventory ConfigMap: @@ -153,7 +189,7 @@ take a look at [Flux](https://github.com/fluxcd/flux2). If you got so far you may wander how is Kustomizer different to running: ```bash -kustomize build . | kubectl apply -f- --prune -l app=my-app +kubectl apply -k ./my-app --prune -l app=my-app ``` The pruning feature in kubectl while still experimental has many downsides, most notable is that pruning diff --git a/cmd/kustomizer/apply.go b/cmd/kustomizer/apply.go index 5ca91ad..a650162 100644 --- a/cmd/kustomizer/apply.go +++ b/cmd/kustomizer/apply.go @@ -44,6 +44,8 @@ type applyFlags struct { wait bool force bool prune bool + source string + revision string } var applyArgs applyFlags @@ -59,6 +61,8 @@ func init() { applyCmd.Flags().StringVarP(&applyArgs.inventoryName, "inventory-name", "i", "", "The name of the inventory configmap.") applyCmd.Flags().StringVar(&applyArgs.inventoryNamespace, "inventory-namespace", "default", "The namespace of the inventory configmap. The namespace must exist on the target cluster.") + applyCmd.Flags().StringVar(&applyArgs.source, "source", "", "The URL to the source code.") + applyCmd.Flags().StringVar(&applyArgs.revision, "revision", "", "The revision identifier.") rootCmd.AddCommand(applyCmd) } @@ -81,6 +85,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { } newInventory := inventory.NewInventory(applyArgs.inventoryName, applyArgs.inventoryNamespace) + newInventory.SetSource(applyArgs.source, applyArgs.revision) if err := newInventory.AddObjects(objects); err != nil { return fmt.Errorf("creating inventory failed, error: %w", err) } diff --git a/cmd/kustomizer/get.go b/cmd/kustomizer/get.go new file mode 100644 index 0000000..b46a843 --- /dev/null +++ b/cmd/kustomizer/get.go @@ -0,0 +1,37 @@ +/* +Copyright 2021 Stefan Prodan + +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 getCmd = &cobra.Command{ + Use: "get", + Short: "Get prints the content of inventories.", +} + +type getFlags struct { + namespace string +} + +var getArgs getFlags + +func init() { + getCmd.Flags().StringVar(&getArgs.namespace, "namespace", "default", "The namespace of the inventory.") + rootCmd.AddCommand(getCmd) +} diff --git a/cmd/kustomizer/get_inventories.go b/cmd/kustomizer/get_inventories.go new file mode 100644 index 0000000..06158a7 --- /dev/null +++ b/cmd/kustomizer/get_inventories.go @@ -0,0 +1,98 @@ +/* +Copyright 2021 Stefan Prodan + +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" + "fmt" + + "github.com/spf13/cobra" + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var getInventories = &cobra.Command{ + Use: "inventories", + Short: "Get prints the content of all inventories in the given namespace.", + RunE: runGetInventoriesCmd, +} + +func init() { + getCmd.AddCommand(getInventories) +} + +func runGetInventoriesCmd(cmd *cobra.Command, args []string) error { + + if getArgs.namespace == "" { + return fmt.Errorf("you must specify an intentory namespace") + } + + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("status poller init failed: %w", err) + } + + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + list := &corev1.ConfigMapList{} + err = resMgr.Client().List(ctx, list, client.InNamespace(getArgs.namespace), client.MatchingLabels{ + "app.kubernetes.io/component": "inventory", + "app.kubernetes.io/created-by": "kustomizer", + }) + if err != nil { + return err + } + + for _, cm := range list.Items { + fmt.Println(fmt.Sprintf("Inventory: %s/%s", cm.GetNamespace(), cm.GetName())) + if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/last-applied-time"]; ok { + fmt.Println(fmt.Sprintf("LastAppliedTime: %s", s)) + } + if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/source"]; ok { + fmt.Println(fmt.Sprintf("Source: %s", s)) + } + if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/revision"]; ok { + fmt.Println(fmt.Sprintf("Revision: %s", s)) + } + i := inventory.NewInventory(cm.GetName(), cm.GetNamespace()) + if err := resMgr.GetInventory(ctx, i); err != nil { + return err + } + + fmt.Println("Entries:") + entries, err := i.ListMeta() + if err != nil { + return err + } + for _, entry := range entries { + fmt.Println("-", objectutil.FmtObjMetadata(entry)) + } + } + + return nil +} diff --git a/cmd/kustomizer/get_inventory.go b/cmd/kustomizer/get_inventory.go new file mode 100644 index 0000000..fc0dbf6 --- /dev/null +++ b/cmd/kustomizer/get_inventory.go @@ -0,0 +1,82 @@ +/* +Copyright 2021 Stefan Prodan + +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" + "fmt" + + "github.com/spf13/cobra" + "github.com/stefanprodan/kustomizer/pkg/inventory" + "github.com/stefanprodan/kustomizer/pkg/manager" + "github.com/stefanprodan/kustomizer/pkg/objectutil" +) + +var getInventoryCmd = &cobra.Command{ + Use: "inventory [name]", + Short: "Get inventory prints the content of the given inventory.", + RunE: runGetInventoryCmd, +} + +func init() { + getCmd.AddCommand(getInventoryCmd) +} + +func runGetInventoryCmd(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("you must specify an intentory name") + } + name := args[0] + if getArgs.namespace == "" { + return fmt.Errorf("you must specify a namespace") + } + + i := inventory.NewInventory(name, getArgs.namespace) + + kubeClient, err := newKubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("client init failed: %w", err) + } + + statusPoller, err := newKubeStatusPoller(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return fmt.Errorf("status poller init failed: %w", err) + } + + resMgr := manager.NewResourceManager(kubeClient, statusPoller, inventoryOwner) + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + if err := resMgr.GetInventory(ctx, i); err != nil { + return err + } + + fmt.Println(fmt.Sprintf("Inventory: %s/%s", i.Namespace, i.Name)) + fmt.Println(fmt.Sprintf("Source: %s", i.Source)) + fmt.Println(fmt.Sprintf("Revision: %s", i.Revision)) + fmt.Println("Entries:") + entries, err := i.ListMeta() + if err != nil { + return err + } + for _, entry := range entries { + fmt.Println("-", objectutil.FmtObjMetadata(entry)) + } + + return nil +} diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go index 8aabc99..cecb7ac 100644 --- a/pkg/inventory/inventory.go +++ b/pkg/inventory/inventory.go @@ -30,10 +30,16 @@ import ( // Inventory is a record of objects that are applied on a cluster stored as a configmap. type Inventory struct { // Name of the inventory configmap. - Name string + Name string `json:"name"` // Namespace of the inventory configmap. - Namespace string + Namespace string `json:"namespace"` + + // Source is the URL of the source code. + Source string `json:"source,omitempty"` + + // Revision is the Source control revision identifier. + Revision string `json:"revision,omitempty"` // Entries of Kubernetes objects metadata. Entries []Entry `json:"entries"` @@ -58,6 +64,12 @@ func NewInventory(name, namespace string) *Inventory { } } +// SetSource sets the source url and revision for this inventory. +func (inv *Inventory) SetSource(url, revision string) { + inv.Source = url + inv.Revision = revision +} + // AddObjects extracts the metadata from the given objects and adds it to the inventory. func (inv *Inventory) AddObjects(objects []*unstructured.Unstructured) error { sort.Sort(objectutil.SortableUnstructureds(objects)) diff --git a/pkg/manager/doc.go b/pkg/manager/doc.go index 2b580f8..8a1810d 100644 --- a/pkg/manager/doc.go +++ b/pkg/manager/doc.go @@ -17,14 +17,13 @@ limitations under the License. // Package manager contains utilities for managing Kubernetes resources. // -// The ResourceManager performs the following actions: -// - orders the Kubernetes objects for apply (CRDs, Namespaces, ClusterRoles first) +// The ResourceManager can be used to write a GitOps reconciler that: +// - maintains an inventory of objects applied on the cluster +// - orders the Kubernetes objects for apply (CRDs, Namespaces first, Webhooks last) // - validates the objects with server-side dry-run apply // - determines if the in-cluster objects are in drift based on the dry-run result -// - reconciles the objects on the cluster with server-side apply +// - applies the objects on the cluster with server-side apply // - waits for the objects to be fully reconciled by looking up their readiness status // - deletes objects that are subject to garbage collection // - waits for the deleted objects to be terminated -// - maintains an inventory of objects applied on the cluster -// - performs garbage collection of stale objects based on the inventory entries package manager diff --git a/pkg/manager/manager_inventory.go b/pkg/manager/manager_inventory.go index ae46b62..0c09164 100644 --- a/pkg/manager/manager_inventory.go +++ b/pkg/manager/manager_inventory.go @@ -43,6 +43,13 @@ func (m *ResourceManager) ApplyInventory(ctx context.Context, i *inventory.Inven cm.Annotations = map[string]string{ m.owner.Group + "/last-applied-time": time.Now().UTC().Format(time.RFC3339), } + if i.Source != "" { + cm.Annotations[m.owner.Group+"/source"] = i.Source + } + if i.Revision != "" { + cm.Annotations[m.owner.Group+"/revision"] = i.Revision + } + cm.Data = map[string]string{ inventoryKindName: string(data), } @@ -75,6 +82,16 @@ func (m *ResourceManager) GetInventory(ctx context.Context, i *inventory.Invento } i.Entries = entries + + for k, v := range cm.GetAnnotations() { + switch k { + case m.owner.Group + "/source": + i.Source = v + case m.owner.Group + "/revision": + i.Revision = v + } + } + return nil } From 47a81fde41b58ba1485c7b849b6d133dc78e2428 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 7 Sep 2021 09:42:05 +0300 Subject: [PATCH 40/40] List inventories in tabular view Signed-off-by: Stefan Prodan --- README.md | 57 +++++++++++++++++++++++++------ cmd/kustomizer/get_inventories.go | 49 ++++++++++++++++++-------- go.mod | 1 + go.sum | 2 ++ 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1b252d4..43c1e20 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,13 @@ objects that were previously applied but are missing from the current revision. Compared to `kubectl apply`, Kustomizer does things a little different: - Applies first custom resource definitions (CRDs) and namespaces, waits for them to register and only then applies the custom resources. -- Skips to apply resources that haven't changed - (determined with Kubernetes API [server-side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) dry-run). +- Skips to apply resources that haven't changed. - Waits for the applied resources to be fully reconciled (waits for replicasets rollout, ingress and other custom resources to become ready). - Deletes stale objects like ConfigMap and Secrets generated with Kustomize or other tools. +Kustomizer relies on Kubernetes API [server-side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) +and [kstatus](https://pkg.go.dev/sigs.k8s.io/cli-utils/pkg/kstatus). + ## Install The Kustomizer CLI is available as a binary executable for all major platforms, @@ -33,11 +35,11 @@ Kustomizer needs a Kubernetes cluster version **1.18** or later and a valid `kub The Kustomize CLI comes with the following commands: -* `apply` Apply validates the given Kubernetes manifests or Kustomize overlays and reconciles them using server-side apply. * `build` Build scans the given path for Kubernetes manifests or Kustomize overlays and prints the YAML multi-doc to stdout. -* `delete` Delete removes the Kubernetes objects in the inventory from the cluster and waits for termination. -* `diff` Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff to stdout. +* `apply` Apply validates the given Kubernetes manifests or Kustomize overlays and reconciles them using server-side apply. * `get` Prints the content of inventories and their source revision. +* `diff` Diff compares the local Kubernetes manifests with the in-cluster objects and prints the YAML diff to stdout. +* `delete` Delete removes the Kubernetes objects in the inventory from the cluster and waits for termination. ## Get Started @@ -80,13 +82,22 @@ waits for the workloads to be rollout. To apply Kustomize overlays, you can use `kustomizer apply -k path/to/overlay`, for more details see `--help`. After applying the resources, Kustomizer creates an inventory. -You can list the content of an inventory with: +You cal list all inventories in a specific namespace with: + +```console +$ kustomizer get inventories --namespace default + +NAME ENTRIES SOURCE REVISION LAST APPLIED +demo 10 https://github.com/stefanprodan/kustomizer.git e44c210 2021-09-06T16:33:08Z +``` + +You can list the Kubernetes objects in an inventory with: ```console $ kustomizer get inventory demo Inventory: default/demo -LastAppliedTime: 2021-09-06T16:33:08Z +LastApplied: 2021-09-06T16:33:08Z Source: https://github.com/stefanprodan/kustomizer.git Revision: e44c210 Entries: @@ -102,16 +113,42 @@ Entries: - HorizontalPodAutoscaler/kustomizer-demo/frontend ``` -The inventory entries are used to track which objects are subject to garbage collection. +The inventory records are used to track which objects are subject to garbage collection. The inventory is persistent on the cluster as a ConfigMap. -Remove the `frontend` and the `rbac` manifests from the local dir: +Change the min replicas of the `backend` HPA and remove the `frontend` and the `rbac` manifests from the local dir: ```bash rm -rf testdata/plain/frontend rm -rf testdata/plain/common/rbac.yaml ``` +Preview the changes using diff: + +```console +$ kustomizer diff -i demo -f ./testdata/plain/ --prune + +► HorizontalPodAutoscaler/kustomizer-demo/backend drifted +  ( +   """ +   ... // 18 identical lines +   type: Utilization +   type: Resource +-  minReplicas: 2 ++  minReplicas: 1 +   scaleTargetRef: +   apiVersion: apps/v1 +   ... // 32 identical lines +   """ +  ) + +► ClusterRole/kustomizer-demo-read-only deleted +► ClusterRoleBinding/kustomizer-demo-read-only deleted +► Service/kustomizer-demo/frontend deleted +► Deployment/kustomizer-demo/frontend deleted +► HorizontalPodAutoscaler/kustomizer-demo/frontend deleted +``` + Rerun the apply command: ```console @@ -123,7 +160,7 @@ Namespace/kustomizer-demo unchanged ServiceAccount/kustomizer-demo/demo unchanged Service/kustomizer-demo/backend unchanged Deployment/kustomizer-demo/backend unchanged -HorizontalPodAutoscaler/kustomizer-demo/backend unchanged +HorizontalPodAutoscaler/kustomizer-demo/backend configured HorizontalPodAutoscaler/kustomizer-demo/frontend deleted Deployment/kustomizer-demo/frontend deleted Service/kustomizer-demo/frontend deleted diff --git a/cmd/kustomizer/get_inventories.go b/cmd/kustomizer/get_inventories.go index 06158a7..30dffaf 100644 --- a/cmd/kustomizer/get_inventories.go +++ b/cmd/kustomizer/get_inventories.go @@ -19,11 +19,14 @@ package main import ( "context" "fmt" + "io" + "os" - "github.com/spf13/cobra" "github.com/stefanprodan/kustomizer/pkg/inventory" "github.com/stefanprodan/kustomizer/pkg/manager" - "github.com/stefanprodan/kustomizer/pkg/objectutil" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -68,31 +71,47 @@ func runGetInventoriesCmd(cmd *cobra.Command, args []string) error { return err } + var rows [][]string for _, cm := range list.Items { - fmt.Println(fmt.Sprintf("Inventory: %s/%s", cm.GetNamespace(), cm.GetName())) + var ts string + var source string + var rev string if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/last-applied-time"]; ok { - fmt.Println(fmt.Sprintf("LastAppliedTime: %s", s)) + ts = s } if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/source"]; ok { - fmt.Println(fmt.Sprintf("Source: %s", s)) + source = s } if s, ok := cm.GetAnnotations()["inventory.kustomizer.dev/revision"]; ok { - fmt.Println(fmt.Sprintf("Revision: %s", s)) + rev = s } i := inventory.NewInventory(cm.GetName(), cm.GetNamespace()) if err := resMgr.GetInventory(ctx, i); err != nil { return err } - - fmt.Println("Entries:") - entries, err := i.ListMeta() - if err != nil { - return err - } - for _, entry := range entries { - fmt.Println("-", objectutil.FmtObjMetadata(entry)) - } + row := []string{cm.GetName(), fmt.Sprintf("%v", len(i.Entries)), source, rev, ts} + rows = append(rows, row) } + printTable(os.Stdout, []string{"name", "entries", "source", "revision", "last applied"}, rows) + return nil } + +func printTable(writer io.Writer, header []string, rows [][]string) { + table := tablewriter.NewWriter(writer) + table.SetHeader(header) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + table.SetNoWhiteSpace(true) + table.AppendBulk(rows) + table.Render() +} diff --git a/go.mod b/go.mod index 0fcdca2..d9c87a3 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/google/go-cmp v0.5.5 + github.com/olekukonko/tablewriter v0.0.4 github.com/spf13/cobra v1.1.3 k8s.io/api v0.22.1 k8s.io/apiextensions-apiserver v0.22.1 diff --git a/go.sum b/go.sum index 87b482c..34de6b2 100644 --- a/go.sum +++ b/go.sum @@ -418,6 +418,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO 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-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= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= @@ -464,6 +465,7 @@ github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtb github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=