diff --git a/cmd/flux/build.go b/cmd/flux/build.go new file mode 100644 index 0000000000..0c901036ec --- /dev/null +++ b/cmd/flux/build.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" +) + +var buildCmd = &cobra.Command{ + Use: "build", + Short: "Build a flux resource", + Long: "The build command is used to build flux resources.", +} + +func init() { + rootCmd.AddCommand(buildCmd) +} diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go new file mode 100644 index 0000000000..966046b7d6 --- /dev/null +++ b/cmd/flux/build_kustomization.go @@ -0,0 +1,100 @@ +/* +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" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/build" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" +) + +var buildKsCmd = &cobra.Command{ + Use: "kustomization", + Aliases: []string{"ks"}, + Short: "Build Kustomization", + Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. +It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml +pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, + Example: `# Build the local manifests as they were built on the cluster +flux build kustomization my-app --path ./path/to/local/manifests`, + ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), + RunE: buildKsCmdRun, +} + +type buildKsFlags struct { + path string +} + +var buildKsArgs buildKsFlags + +func init() { + buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.)") + buildCmd.AddCommand(buildKsCmd) +} + +func buildKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if buildKsArgs.path == "" { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) + } + + if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) + } + + builder, err := build.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout)) + if err != nil { + return err + } + + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + manifests, err := builder.Build() + if err != nil { + errChan <- err + } + + cmd.Print(string(manifests)) + errChan <- nil + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } + } + + return nil + +} diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go new file mode 100644 index 0000000000..99ecb45c7a --- /dev/null +++ b/cmd/flux/build_kustomization_test.go @@ -0,0 +1,83 @@ +//go:build unit +// +build unit + +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func setup(t *testing.T, tmpl map[string]string) { + t.Helper() + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t) +} + +func TestBuildKustomization(t *testing.T) { + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "build kustomization podinfo", + resultFile: "invalid resource path \"\"", + assertFunc: "assertError", + }, + { + name: "build podinfo", + args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build podinfo without service", + args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", + resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/diff.go b/cmd/flux/diff.go new file mode 100644 index 0000000000..d4ee4804e8 --- /dev/null +++ b/cmd/flux/diff.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" +) + +var diffCmd = &cobra.Command{ + Use: "diff", + Short: "Diff a flux resource", + Long: "The diff command is used to do a server-side dry-run on flux resources, then output the diff.", +} + +func init() { + rootCmd.AddCommand(diffCmd) +} diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go new file mode 100644 index 0000000000..e40f72c93d --- /dev/null +++ b/cmd/flux/diff_kustomization.go @@ -0,0 +1,98 @@ +/* +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" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/build" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" +) + +var diffKsCmd = &cobra.Command{ + Use: "kustomization", + Aliases: []string{"ks"}, + Short: "Diff Kustomization", + Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, + Example: `# Preview changes local changes as they were applied on the cluster +flux diff kustomization my-app --path ./path/to/local/manifests`, + ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), + RunE: diffKsCmdRun, +} + +type diffKsFlags struct { + path string +} + +var diffKsArgs diffKsFlags + +func init() { + diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.)") + diffCmd.AddCommand(diffKsCmd) +} + +func diffKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if diffKsArgs.path == "" { + return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + } + + if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + } + + builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) + if err != nil { + return err + } + + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + output, err := builder.Diff() + if err != nil { + errChan <- err + } + + cmd.Print(output) + errChan <- nil + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } + } + + return nil + +} diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go new file mode 100644 index 0000000000..e2f62e75f2 --- /dev/null +++ b/cmd/flux/diff_kustomization_test.go @@ -0,0 +1,129 @@ +//go:build unit +// +build unit + +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/fluxcd/flux2/internal/build" + "github.com/fluxcd/pkg/ssa" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestDiffKustomization(t *testing.T) { + tests := []struct { + name string + args string + objectFile string + assert assertFunc + }{ + { + name: "no args", + args: "diff kustomization podinfo", + objectFile: "", + assert: assertError("invalid resource path \"\""), + }, + { + name: "diff nothing deployed", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "", + assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), + }, + { + name: "diff with a deployment object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/deployment.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-deployment.golden"), + }, + { + name: "diff with a drifted service object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/service.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-service.golden"), + }, + { + name: "diff with a drifted secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-secret.golden"), + }, + { + name: "diff with a drifted key in sops secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/key-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden"), + }, + { + name: "diff with a drifted value in sops secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"), + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + + b, _ := build.NewBuilder(kubeconfigArgs, "podinfo", "") + + resourceManager, err := b.Manager() + if err != nil { + t.Fatal(err) + } + + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.objectFile != "" { + resourceManager.ApplyAll(context.Background(), createObjectFromFile(tt.objectFile, tmpl, t), ssa.DefaultApplyOptions()) + } + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: tt.assert, + } + cmd.runTestCmd(t) + if tt.objectFile != "" { + testEnv.DeleteObjectFile(tt.objectFile, tmpl, t) + } + }) + } +} + +func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) + } + + return clientObjects +} diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 10879e23d0..26b4e99e13 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -49,8 +49,8 @@ func allocateNamespace(prefix string) string { return fmt.Sprintf("%s-%d", prefix, id) } -func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { - objects := []unstructured.Unstructured{} +func readYamlObjects(rdr io.Reader) ([]*unstructured.Unstructured, error) { + objects := []*unstructured.Unstructured{} reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr)) for { doc, err := reader.Read() @@ -65,7 +65,7 @@ func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { if err != nil { return nil, err } - objects = append(objects, *unstructuredObj) + objects = append(objects, unstructuredObj) } return objects, nil } @@ -96,7 +96,7 @@ func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues } } -func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured, t *testing.T) error { +func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { for _, obj := range clientObjects { // First create the object then set its status if present in the // yaml file. Make a copy first since creating an object may overwrite @@ -107,7 +107,7 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstruct return err } obj.SetResourceVersion(createObj.GetResourceVersion()) - err = m.client.Status().Update(context.Background(), &obj) + err = m.client.Status().Update(context.Background(), obj) if err != nil { return err } @@ -115,6 +115,36 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstruct return nil } +func (m *testEnvKubeManager) DeleteObjectFile(objectFile string, templateValues map[string]string, t *testing.T) { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) + } + err = m.DeleteObjects(clientObjects, t) + if err != nil { + t.Logf("Error deleting test objects: '%v'", err) + } +} + +func (m *testEnvKubeManager) DeleteObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { + for _, obj := range clientObjects { + err := m.client.Delete(context.Background(), obj) + if err != nil { + return err + } + } + + return nil +} + func (m *testEnvKubeManager) Stop() error { if m.testEnv == nil { return fmt.Errorf("do nothing because testEnv is nil") diff --git a/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml new file mode 100644 index 0000000000..25896dd87d --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.3 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml new file mode 100644 index 0000000000..6bd46a2c42 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 diff --git a/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml new file mode 100644 index 0000000000..1d0e99c5f6 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml diff --git a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml new file mode 100644 index 0000000000..6d3eabedea --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default diff --git a/cmd/flux/testdata/build-kustomization/podinfo-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml new file mode 100644 index 0000000000..ce66ff0f58 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml @@ -0,0 +1,148 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + progressDeadlineSeconds: 60 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: podinfo + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + prometheus.io/port: "9797" + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: '#34577c' + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + name: podinfod + ports: + - containerPort: 9898 + name: http + protocol: TCP + - containerPort: 9797 + name: http-metrics + protocol: TCP + - containerPort: 9999 + name: grpc + protocol: TCP + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi +--- +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo +--- +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - name: grpc + port: 9999 + protocol: TCP + targetPort: grpc + selector: + app: podinfo + type: ClusterIP +--- +apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque +--- +apiVersion: v1 +data: + password: MWYyZDFlMmU2N2Rm + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: db-user-pass-bkbd782d2c + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml new file mode 100644 index 0000000000..745dd65064 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1beta1 +kind: GitRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 30s + ref: + branch: master + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml new file mode 100644 index 0000000000..943381c885 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + progressDeadlineSeconds: 60 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: podinfo + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + prometheus.io/port: "9797" + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: '#34577c' + image: ghcr.io/stefanprodan/podinfo:6.0.3 + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + name: podinfod + ports: + - containerPort: 9898 + name: http + protocol: TCP + - containerPort: 9797 + name: http-metrics + protocol: TCP + - containerPort: 9999 + name: grpc + protocol: TCP + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi +--- +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml new file mode 100644 index 0000000000..d96b3c3c68 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml new file mode 100644 index 0000000000..6bd46a2c42 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 diff --git a/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml new file mode 100644 index 0000000000..0ba076685d --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +secretGenerator: + - files: + - token=token.encrypted + name: podinfo-token + - literals: + - username=admin + - password=1f2d1e2e67df + name: db-user-pass diff --git a/cmd/flux/testdata/build-kustomization/podinfo/service.yaml b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml new file mode 100644 index 0000000000..9450823d5a --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: podinfo +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted new file mode 100644 index 0000000000..c88ac97278 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted @@ -0,0 +1,20 @@ + { + "data": "ENC[AES256_GCM,data:oBe5PlPmfQCUUc4sqKImjw==,iv:MLLEW15QC9kRdVVagJnzLCSk0xZGWIpAeTfHzyxT10g=,tag:K3GkBCGS+ut4Tpk6ndb0CA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+ IFgyNTUxOSA1L2RpZHRrK1FSVmYrd1Va\nY0hxWFQzSDBsT1k3WjNtYmU1QmliaDJycXlNCnF1YjdNOThVbVNvMG9rNS9ZUXZw\nMnV0bnRUMGNtejFPbzM4U2UzWkszeVkKLS0tIGJ6UGhxMUV3YmVJTHlJSUJpRVRZ\nVjd0RVRadU8wekxXTHIrYUplYkN2aEEK0I/ MCEtXRk+b/N2G1JF3vHQT24dShWYD\nw+JIUSA3aLf2sv0zr2MdUEdVWBJoM8nT4D4xVbBORD+669W+9nDeSw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2021-11-26T16:34:51Z", + "mac": "ENC[AES256_GCM,data:COGzf5YCHNNP6z4JaEKrjN3M8f5+Q1uKUKTMHwj388/ICmLyi2sSrTmj7PP+X7M9jTVwa8wVgYTpNLiVJx+LcxqvIXM0Tyo+/Cu1zrfao98aiACP8+TSEDiFQNtEus23H+d/X1hqMwRHDI3kQ+ 6scgEGnqY57r3RDSA3E8EhHr4=,iv:LxitVIYm8srZVqFueJh9loClA44Y2Z3XAVYmxesMmOg=,tag:Y8qFD8UGlDfwNSv7xlcn6A==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.1" + } + } \ No newline at end of file diff --git a/cmd/flux/testdata/diff-kustomization/deployment.yaml b/cmd/flux/testdata/diff-kustomization/deployment.yaml new file mode 100644 index 0000000000..3910da6ac7 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden new file mode 100644 index 0000000000..098497fccd --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden @@ -0,0 +1,4 @@ +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden new file mode 100644 index 0000000000..3826954479 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden @@ -0,0 +1,10 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 drifted + +data + - one map entry removed: + one map entry added: + drift-key: "*****" token: "*****" + +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden new file mode 100644 index 0000000000..ac76978f18 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden @@ -0,0 +1,11 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c drifted + +data.password + ± value change + - ****** + + ***** + diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden new file mode 100644 index 0000000000..d65e5968e4 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden @@ -0,0 +1,16 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo drifted + +spec.ports + ⇆ order changed + - http, grpc + + grpc, http + +spec.ports.http.port + ± value change + - 9899 + + 9898 + +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden new file mode 100644 index 0000000000..033db67e5d --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden @@ -0,0 +1,4 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml new file mode 100644 index 0000000000..52f7cf46f0 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + drift-key: bXktc2VjcmV0LXRva2VuCg== +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/kustomization.yaml b/cmd/flux/testdata/diff-kustomization/kustomization.yaml new file mode 100644 index 0000000000..997ce70af3 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +secretGenerator: + - literals: + - username=admin + - password=1f2d1e2e67df + name: secret-basic-auth diff --git a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden new file mode 100644 index 0000000000..da1c23dae0 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden @@ -0,0 +1,5 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/secret.yaml b/cmd/flux/testdata/diff-kustomization/secret.yaml new file mode 100644 index 0000000000..cf0b351e22 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: db-user-pass-bkbd782d2c + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/service.yaml b/cmd/flux/testdata/diff-kustomization/service.yaml new file mode 100644 index 0000000000..9506816228 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9899 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml new file mode 100644 index 0000000000..3ae9ada209 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + token: ZHJpZnQtdmFsdWUK +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque diff --git a/go.mod b/go.mod index 4bfb0a14ca..2ef0441cf0 100644 --- a/go.mod +++ b/go.mod @@ -12,16 +12,23 @@ require ( github.com/fluxcd/image-reflector-controller/api v0.15.0 github.com/fluxcd/kustomize-controller/api v0.19.1 github.com/fluxcd/notification-controller/api v0.20.1 + github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect github.com/fluxcd/pkg/apis/meta v0.10.2 github.com/fluxcd/pkg/runtime v0.12.3 - github.com/fluxcd/pkg/ssa v0.10.0 + github.com/fluxcd/pkg/ssa v0.11.0 github.com/fluxcd/pkg/ssh v0.3.1 github.com/fluxcd/pkg/untar v0.0.5 github.com/fluxcd/pkg/version v0.0.1 github.com/fluxcd/source-controller/api v0.20.1 github.com/go-git/go-git/v5 v5.4.2 + github.com/gonvenience/bunt v1.3.2 + github.com/gonvenience/ytbx v1.4.2 github.com/google/go-cmp v0.5.6 github.com/google/go-containerregistry v0.2.0 + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.0 // indirect + github.com/homeport/dyff v1.4.6 + github.com/lucasb-eyer/go-colorful v1.2.0 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-shellwords v1.0.12 github.com/olekukonko/tablewriter v0.0.4 @@ -41,6 +48,11 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require ( + github.com/fluxcd/pkg/kustomize v0.0.2 + sigs.k8s.io/kustomize/kyaml v0.13.0 +) + require ( cloud.google.com/go v0.81.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -50,6 +62,7 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/BurntSushi/toml v0.4.1 // indirect github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect github.com/Microsoft/go-winio v0.4.16 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -59,11 +72,11 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect - github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect @@ -75,6 +88,10 @@ require ( github.com/go-openapi/swag v0.19.14 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/gonvenience/neat v1.3.7 // indirect + github.com/gonvenience/term v1.0.1 // indirect + github.com/gonvenience/text v1.0.6 // indirect + github.com/gonvenience/wrap v1.1.0 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-github/v41 v41.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -85,8 +102,6 @@ require ( github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect @@ -95,9 +110,13 @@ require ( github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -108,15 +127,18 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/stretchr/testify v1.7.0 // indirect + github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect github.com/xanzy/go-gitlab v0.54.3 // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect @@ -131,7 +153,6 @@ require ( k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect - sigs.k8s.io/kustomize/kyaml v0.13.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect ) diff --git a/go.sum b/go.sum index 17b4f8204e..9f897fafc5 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= @@ -192,6 +194,8 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= +github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= @@ -236,10 +240,12 @@ github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVf github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc= github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= +github.com/fluxcd/pkg/kustomize v0.0.2 h1:ipvQrxSeuGZDsPZrVUL6tYMlTR5xqYTZp6G0Tdy2hVs= +github.com/fluxcd/pkg/kustomize v0.0.2/go.mod h1:AFwnf3OqQmpTCuwCARTGpPRMBf0ZFJNGCvW63KbgK04= github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= -github.com/fluxcd/pkg/ssa v0.10.0 h1:dhgWDeqz0/zAs5guzmPx/DMPCkzZdlEiPvCs1NChAQM= -github.com/fluxcd/pkg/ssa v0.10.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= +github.com/fluxcd/pkg/ssa v0.11.0 h1:ejEMlHPsbXMP8BJQx3+0PwoBgJur0mHiPcMNKcFwoOE= +github.com/fluxcd/pkg/ssa v0.11.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244= github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ= github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk= @@ -359,6 +365,22 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gonvenience/bunt v1.1.3/go.mod h1:hZ898ZprNWgaVlq4s1ivsJu3AY+3wrlJadF5Gp7Yq2c= +github.com/gonvenience/bunt v1.3.1/go.mod h1:G+d3dJBxxOqV2oca96psgAnPABVC9QptEifjVqPu+mo= +github.com/gonvenience/bunt v1.3.2 h1:gDiyTDfPf87fCtIbFzvENrmlnDZYbENdhhRW2kun/tw= +github.com/gonvenience/bunt v1.3.2/go.mod h1:oTOZqb1TVL1KqZm57zUCJCiwUliNfY8+d3QndOVqxpg= +github.com/gonvenience/neat v1.3.6/go.mod h1:wv0eXmvwFfpuS5bpf2fIofXQvf8W7HTdSzKfGCYbIe8= +github.com/gonvenience/neat v1.3.7 h1:k4shy3sgSBfUk9LTN51naxVIkB6BlGaxH0ReERO92zw= +github.com/gonvenience/neat v1.3.7/go.mod h1:Y4eeQH3GEBvjmLoMiu4oWGyOopGDaI2/y2jwcVfvlvs= +github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= +github.com/gonvenience/term v1.0.1 h1:8bg2O0ox0Ss64nnUxW5AXlSHhllc8dTOSKuKu6uoGpw= +github.com/gonvenience/term v1.0.1/go.mod h1:TrQEhxBNE/ng5kTV+S0OvQowTDJSfhlBeZbcOmTR6qI= +github.com/gonvenience/text v1.0.6 h1:9JH9fz0BL0NX4uKmjLuVcsBKiniPa+XLpf8KH9so44U= +github.com/gonvenience/text v1.0.6/go.mod h1:9U5WbkT/5wR5+aNMR4HucamY+HgVMEn+UbF78XHmUio= +github.com/gonvenience/wrap v1.1.0 h1:d8gEZrXS/zg4BC1q0U4nHpPIh5k6muKpQ1+rQFBwpYc= +github.com/gonvenience/wrap v1.1.0/go.mod h1:L47Cm1sK1G8QmFAYQfkHcF/sQ1IBJUa0u4sjqiLqPdM= +github.com/gonvenience/ytbx v1.4.2 h1:fpgOpsQ+gwTPqiatki0aY7q3BEjt7EcwiI5b+D0Qjvg= +github.com/gonvenience/ytbx v1.4.2/go.mod h1:GkUMPGH5qZSg1S8L6u9XNI9hJ4L1yKSQFIA4J8vaPdY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -422,6 +444,7 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -449,8 +472,9 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -465,6 +489,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/homeport/dyff v1.4.6 h1:ReC8Hi1I6SkmPmNOqGl9EUlZctx+6AloCzSulqwHge8= +github.com/homeport/dyff v1.4.6/go.mod h1:DBCaTwJUIQLNQxNOKTce/OgRxCwwa8erBdN88bBfb9Y= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -523,6 +549,9 @@ github.com/ktrysmt/go-bitbucket v0.9.34/go.mod h1:FWxy2UK7GlK5b0NSJGc5hPqnssVlkN github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -536,9 +565,15 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= +github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -553,10 +588,15 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= +github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -597,6 +637,11 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= @@ -606,6 +651,10 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= +github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= @@ -667,8 +716,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -717,12 +767,16 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= +github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db/go.mod h1:grWy0bkr1XO6hqbaaCKaPXqkBVlMGHYG6PGykktwbJc= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= +github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/xanzy/go-gitlab v0.54.3 h1:fPfZ3Jcu5dPc3xyIYtAALZsEgoyKNFNuULD+TdJ7Zvk= github.com/xanzy/go-gitlab v0.54.3/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= @@ -862,6 +916,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -877,6 +932,7 @@ golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917163549-3c21e5b27794/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -909,6 +965,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -922,6 +979,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -945,6 +1003,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -987,11 +1046,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/build/build.go b/internal/build/build.go new file mode 100644 index 0000000000..075ec44c66 --- /dev/null +++ b/internal/build/build.go @@ -0,0 +1,297 @@ +/* +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 build + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "sync" + "time" + + "github.com/fluxcd/flux2/internal/utils" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/kustomize" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +const ( + controllerName = "kustomize-controller" + controllerGroup = "kustomize.toolkit.fluxcd.io" + mask = "**SOPS**" +) + +var defaultTimeout = 80 * time.Second + +// Builder builds yaml manifests +// It retrieves the kustomization object from the k8s cluster +// and overlays the manifests with the resources specified in the resourcesPath +type Builder struct { + client client.WithWatch + restMapper meta.RESTMapper + name string + namespace string + resourcesPath string + // mu is used to synchronize access to the kustomization file + mu sync.Mutex + action kustomize.Action + kustomization *kustomizev1.Kustomization + timeout time.Duration +} + +type BuilderOptionFunc func(b *Builder) error + +func WithTimeout(timeout time.Duration) BuilderOptionFunc { + return func(b *Builder) error { + b.timeout = timeout + return nil + } +} + +// NewBuilder returns a new Builder +// to dp : create functional options +func NewBuilder(rcg *genericclioptions.ConfigFlags, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { + kubeClient, err := utils.KubeClient(rcg) + if err != nil { + return nil, err + } + + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return nil, err + } + + b := &Builder{ + client: kubeClient, + restMapper: restMapper, + name: name, + namespace: *rcg.Namespace, + resourcesPath: resources, + } + + for _, opt := range opts { + if err := opt(b); err != nil { + return nil, err + } + } + + if b.timeout == 0 { + b.timeout = defaultTimeout + } + + return b, nil +} + +func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) { + namespacedName := types.NamespacedName{ + Namespace: b.namespace, + Name: b.name, + } + + k := &kustomizev1.Kustomization{} + err := b.client.Get(ctx, namespacedName, k) + if err != nil { + return nil, err + } + + return k, nil +} + +// Build builds the yaml manifests from the kustomization object +// and overlays the manifests with the resources specified in the resourcesPath +// It expects a kustomization.yaml file in the resourcesPath, and it will +// generate a kustomization.yaml file if it doesn't exist +func (b *Builder) Build() ([]byte, error) { + m, err := b.build() + if err != nil { + return nil, err + } + + resources, err := m.AsYaml() + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + return resources, nil +} + +func (b *Builder) build() (m resmap.ResMap, err error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + // Get the kustomization object + k, err := b.getKustomization(ctx) + if err != nil { + return + } + + // store the kustomization object + b.kustomization = k + + // generate kustomization.yaml if needed + action, er := b.generate(*k, b.resourcesPath) + if er != nil { + errf := kustomize.CleanDirectory(b.resourcesPath, action) + err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) + return + } + + b.action = action + + defer func() { + errf := b.Cancel() + if err == nil { + err = errf + } + }() + + // build the kustomization + m, err = b.do(ctx, *k, b.resourcesPath) + if err != nil { + return + } + + for _, res := range m.Resources() { + // set owner labels + err = b.setOwnerLabels(res) + if err != nil { + return + } + + // make sure secrets are masked + err = trimSopsData(res) + if err != nil { + return + } + } + + return + +} + +func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) + if err != nil { + return "", err + } + gen := kustomize.NewGenerator(unstructured.Unstructured{Object: data}) + + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) +} + +func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { + fs := filesys.MakeFsOnDisk() + + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + m, err := kustomize.BuildKustomization(fs, dirPath) + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + for _, res := range m.Resources() { + // run variable substitutions + if kustomization.Spec.PostBuild != nil { + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) + if err != nil { + return nil, err + } + outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res) + if err != nil { + return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) + } + + if outRes != nil { + _, err = m.Replace(res) + if err != nil { + return nil, err + } + } + } + } + + return m, nil +} + +func (b *Builder) setOwnerLabels(res *resource.Resource) error { + labels := res.GetLabels() + + labels[controllerGroup+"/name"] = b.kustomization.GetName() + labels[controllerGroup+"/namespace"] = b.kustomization.GetNamespace() + + err := res.SetLabels(labels) + if err != nil { + return err + } + + return nil +} + +func trimSopsData(res *resource.Resource) error { + // sopsMess is the base64 encoded mask + sopsMess := base64.StdEncoding.EncodeToString([]byte(mask)) + + if res.GetKind() == "Secret" { + dataMap := res.GetDataMap() + for k, v := range dataMap { + data, err := base64.StdEncoding.DecodeString(v) + if err != nil { + if _, ok := err.(base64.CorruptInputError); ok { + return fmt.Errorf("failed to decode secret data: %w", err) + } + } + + if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { + dataMap[k] = sopsMess + } + } + + res.SetDataMap(dataMap) + } + + return nil +} + +// Cancel cancels the build +// It restores a clean reprository +func (b *Builder) Cancel() error { + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + err := kustomize.CleanDirectory(b.resourcesPath, b.action) + if err != nil { + return err + } + + return nil +} diff --git a/internal/build/build_test.go b/internal/build/build_test.go new file mode 100644 index 0000000000..c19a81be5b --- /dev/null +++ b/internal/build/build_test.go @@ -0,0 +1,120 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package build + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestTrimSopsData(t *testing.T) { + testCases := []struct { + name string + yamlStr string + expected string + }{ + { + name: "secret with sops token", + yamlStr: `apiVersion: v1 +kind: Secret +metadata: + name: my-secret +type: Opaque +data: + token: | + ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpvQmU1UGxQbWZRQ1VVYzRzcUtJbW + p3PT0saXY6TUxMRVcxNVFDOWtSZFZWYWdKbnpMQ1NrMHhaR1dJcEFlVGZIenl4VDEwZz0s + dGFnOkszR2tCQ0dTK3V0NFRwazZuZGIwQ0E9PSx0eXBlOnN0cl0iLAoJInNvcHMiOiB7Cg + kJImttcyI6IG51bGwsCgkJImdjcF9rbXMiOiBudWxsLAoJCSJhenVyZV9rdiI6IG51bGws + CgkJImhjX3ZhdWx0IjogbnVsbCwKCQkiYWdlIjogWwoJCQl7CgkJCQkicmVjaXBpZW50Ij + ogImFnZTEwbGEyZ2Uwd3R2eDNxcjdkYXRxZjdyczR5bmd4c3pkYWw5MjdmczlydWthbXI4 + dTJwc2hzdnR6N2NlIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIE + ZJTEUtLS0tLVxuWVdkbExXVnVZM0o1Y0hScGIyNHViM0puTDNZeENpMCtJRmd5TlRVeE9T + QTFMMlJwWkhScksxRlNWbVlyZDFWYVxuWTBoeFdGUXpTREJzVDFrM1dqTnRZbVUxUW1saW + FESnljWGxOQ25GMVlqZE5PVGhWYlZOdk1HOXJOUzlaVVhad1xuTW5WMGJuUlVNR050ZWpG + UGJ6TTRVMlV6V2tzemVWa0tMUzB0SUdKNlVHaHhNVVYzWW1WSlRIbEpTVUpwUlZSWlxuVm + pkMFJWUmFkVTh3ZWt4WFRISXJZVXBsWWtOMmFFRUswSS9NQ0V0WFJrK2IvTjJHMUpGM3ZI + UVQyNGRTaFdZRFxudytKSVVTQTNhTGYyc3YwenIyTWRVRWRWV0JKb004blQ0RDR4VmJCT1 + JEKzY2OVcrOW5EZVN3PT1cbi0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG4i + CgkJCX0KCQldLAoJCSJsYXN0bW9kaWZpZWQiOiAiMjAyMS0xMS0yNlQxNjozNDo1MVoiLA + oJCSJtYWMiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpDT0d6ZjVZQ0hOTlA2ejRKYUVLcmpO + M004ZjUrUTF1S1VLVE1Id2ozODgvSUNtTHlpMnNTclRtajdQUCtYN005alRWd2E4d1ZnWV + RwTkxpVkp4K0xjeHF2SVhNMFR5bysvQ3UxenJmYW85OGFpQUNQOCtUU0VEaUZRTnRFdXMy + M0grZC9YMWhxTXdSSERJM2tRKzZzY2dFR25xWTU3cjNSRFNBM0U4RWhIcjQ9LGl2Okx4aX + RWSVltOHNyWlZxRnVlSmg5bG9DbEE0NFkyWjNYQVZZbXhlc01tT2c9LHRhZzpZOHFGRDhV + R2xEZndOU3Y3eGxjbjZBPT0sdHlwZTpzdHJdIiwKCQkicGdwIjogbnVsbCwKCQkidW5lbm + NyeXB0ZWRfc3VmZml4IjogIl91bmVuY3J5cHRlZCIsCgkJInZlcnNpb24iOiAiMy43LjEi + Cgl9Cn0= +`, + expected: `apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + name: my-secret +type: Opaque +`, + }, + { + name: "secret with basic auth", + yamlStr: `apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4K +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +`, + expected: `apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4K +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := yaml.Parse(tc.yamlStr) + if err != nil { + t.Fatalf("unable to parse yaml: %v", err) + } + + resource := &resource.Resource{RNode: *r} + err = trimSopsData(resource) + if err != nil { + t.Fatalf("unable to trim sops data: %v", err) + } + + sYaml, err := resource.AsYAML() + if err != nil { + t.Fatalf("unable to convert sanitized resources to yaml: %v", err) + } + if diff := cmp.Diff(string(sYaml), tc.expected); diff != "" { + t.Errorf("unexpected sanitized resources: (-got +want)%v", diff) + } + }) + } +} diff --git a/internal/build/diff.go b/internal/build/diff.go new file mode 100644 index 0000000000..70dbb1168b --- /dev/null +++ b/internal/build/diff.go @@ -0,0 +1,287 @@ +package build + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/ssa" + "github.com/gonvenience/bunt" + "github.com/gonvenience/ytbx" + "github.com/google/go-cmp/cmp" + "github.com/homeport/dyff/pkg/dyff" + "github.com/lucasb-eyer/go-colorful" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "sigs.k8s.io/cli-utils/pkg/object" + "sigs.k8s.io/yaml" +) + +func (b *Builder) Manager() (*ssa.ResourceManager, error) { + statusPoller := polling.NewStatusPoller(b.client, b.restMapper, nil) + owner := ssa.Owner{ + Field: controllerName, + Group: controllerGroup, + } + + return ssa.NewResourceManager(b.client, statusPoller, owner), nil +} + +func (b *Builder) Diff() (string, error) { + output := strings.Builder{} + res, err := b.Build() + if err != nil { + return "", err + } + // convert the build result into Kubernetes unstructured objects + objects, err := ssa.ReadObjects(bytes.NewReader(res)) + if err != nil { + return "", err + } + + resourceManager, err := b.Manager() + if err != nil { + return "", err + } + + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + if err := ssa.SetNativeKindsDefaults(objects); err != nil { + return "", err + } + + // create an inventory of objects to be reconciled + newInventory := newInventory() + for _, obj := range objects { + diffOptions := ssa.DiffOptions{ + Exclusions: map[string]string{ + "kustomize.toolkit.fluxcd.io/reconcile": "disabled", + }, + } + change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions) + if err != nil { + if b.kustomization.Spec.Force && ssa.IsImmutableError(err) { + output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green)) + } else { + output.WriteString(writeString(fmt.Sprintf("✗ %v\n", err), bunt.Red)) + } + continue + } + + // if the object is a sops secret, we need to + // make sure we diff only if the keys are different + if obj.GetKind() == "Secret" && change.Action == string(ssa.ConfiguredAction) { + diffSopsSecret(obj, liveObject, mergedObject, change) + } + + if change.Action == string(ssa.CreatedAction) { + output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) + } + + if change.Action == string(ssa.ConfiguredAction) { + output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke)) + liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) + if err != nil { + return "", err + } + defer cleanupDir(tmpDir) + + err = diff(liveFile, mergedFile, &output) + if err != nil { + return "", err + } + } + + addObjectsToInventory(newInventory, change) + } + + if b.kustomization.Spec.Prune { + oldStatus := b.kustomization.Status.DeepCopy() + if oldStatus.Inventory != nil { + diffObjects, err := diffInventory(oldStatus.Inventory, newInventory) + if err != nil { + return "", err + } + for _, object := range diffObjects { + output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed)) + } + } + } + + return output.String(), nil +} + +func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + return "", "", "", err + } + + liveYAML, _ := yaml.Marshal(liveObject) + liveFile := filepath.Join(tmpDir, "live.yaml") + if err := os.WriteFile(liveFile, liveYAML, 0644); err != nil { + return "", "", "", err + } + + mergedYAML, _ := yaml.Marshal(mergedObject) + mergedFile := filepath.Join(tmpDir, "merged.yaml") + if err := os.WriteFile(mergedFile, mergedYAML, 0644); err != nil { + return "", "", "", err + } + + return liveFile, mergedFile, tmpDir, nil +} + +func writeString(t string, color colorful.Color) string { + return bunt.Style( + t, + bunt.EachLine(), + bunt.Foreground(color), + ) +} + +func cleanupDir(dir string) error { + return os.RemoveAll(dir) +} + +func diff(liveFile, mergedFile string, output io.Writer) error { + from, to, err := ytbx.LoadFiles(liveFile, mergedFile) + if err != nil { + return fmt.Errorf("failed to load input files: %w", err) + } + + report, err := dyff.CompareInputFiles(from, to, + dyff.IgnoreOrderChanges(false), + dyff.KubernetesEntityDetection(true), + ) + if err != nil { + return fmt.Errorf("failed to compare input files: %w", err) + } + + reportWriter := &dyff.HumanReport{ + Report: report, + OmitHeader: true, + } + + if err := reportWriter.WriteReport(output); err != nil { + return fmt.Errorf("failed to print report: %w", err) + } + + return nil +} + +func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { + data := obj.Object["data"] + for _, v := range data.(map[string]interface{}) { + v, err := base64.StdEncoding.DecodeString(v.(string)) + if err != nil { + fmt.Println(err) + } + if bytes.Contains(v, []byte(mask)) { + if liveObject != nil && mergedObject != nil { + change.Action = string(ssa.UnchangedAction) + dataLive := liveObject.Object["data"].(map[string]interface{}) + dataMerged := mergedObject.Object["data"].(map[string]interface{}) + if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" { + change.Action = string(ssa.ConfiguredAction) + } + } + } + } +} + +func keys(m map[string]interface{}) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = k + i++ + } + return keys +} + +// diffInventory returns the slice of objects that do not exist in the target inventory. +func diffInventory(inv *kustomizev1.ResourceInventory, target *kustomizev1.ResourceInventory) ([]*unstructured.Unstructured, error) { + versionOf := func(i *kustomizev1.ResourceInventory, objMetadata object.ObjMetadata) string { + for _, entry := range i.Entries { + if entry.ID == objMetadata.String() { + return entry.Version + } + } + return "" + } + + objects := make([]*unstructured.Unstructured, 0) + aList, err := listMetaInInventory(inv) + if err != nil { + return nil, err + } + + bList, err := listMetaInInventory(target) + if err != nil { + return nil, err + } + + list := aList.Diff(bList) + if len(list) == 0 { + return objects, nil + } + + for _, metadata := range list { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.GroupKind.Group, + Kind: metadata.GroupKind.Kind, + Version: versionOf(inv, metadata), + }) + u.SetName(metadata.Name) + u.SetNamespace(metadata.Namespace) + objects = append(objects, u) + } + + sort.Sort(ssa.SortableUnstructureds(objects)) + return objects, nil +} + +// listMetaInInventory returns the inventory entries as object.ObjMetadata objects. +func listMetaInInventory(inv *kustomizev1.ResourceInventory) (object.ObjMetadataSet, error) { + var metas []object.ObjMetadata + for _, e := range inv.Entries { + m, err := object.ParseObjMetadata(e.ID) + if err != nil { + return metas, err + } + metas = append(metas, m) + } + + return metas, nil +} + +func newInventory() *kustomizev1.ResourceInventory { + return &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{}, + } +} + +// addObjectsToInventory extracts the metadata from the given objects and adds it to the inventory. +func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.ChangeSetEntry) error { + if entry == nil { + return nil + } + + inv.Entries = append(inv.Entries, kustomizev1.ResourceRef{ + ID: entry.ObjMetadata.String(), + Version: entry.GroupVersion, + }) + + return nil +}