diff --git a/cmd/helm.go b/cmd/helm.go index 55d3ccea..ee70d451 100644 --- a/cmd/helm.go +++ b/cmd/helm.go @@ -130,38 +130,50 @@ func compatibleHelm3Version() error { return nil } -func getRelease(release, namespace string) ([]byte, error) { +func getRelease(release, namespace, kubeContext string) ([]byte, error) { args := []string{"get", "manifest", release} if namespace != "" { args = append(args, "--namespace", namespace) } + if kubeContext != "" { + args = append(args, "--kube-context", kubeContext) + } cmd := exec.Command(os.Getenv("HELM_BIN"), args...) return outputWithRichError(cmd) } -func getHooks(release, namespace string) ([]byte, error) { +func getHooks(release, namespace, kubeContext string) ([]byte, error) { args := []string{"get", "hooks", release} if namespace != "" { args = append(args, "--namespace", namespace) } + if kubeContext != "" { + args = append(args, "--kube-context", kubeContext) + } cmd := exec.Command(os.Getenv("HELM_BIN"), args...) return outputWithRichError(cmd) } -func getRevision(release string, revision int, namespace string) ([]byte, error) { +func getRevision(release string, revision int, namespace, kubeContext string) ([]byte, error) { args := []string{"get", "manifest", release, "--revision", strconv.Itoa(revision)} if namespace != "" { args = append(args, "--namespace", namespace) } + if kubeContext != "" { + args = append(args, "--kube-context", kubeContext) + } cmd := exec.Command(os.Getenv("HELM_BIN"), args...) return outputWithRichError(cmd) } -func getChart(release, namespace string) (string, error) { +func getChart(release, namespace, kubeContext string) (string, error) { args := []string{"get", "all", release, "--template", "{{.Release.Chart.Name}}"} if namespace != "" { args = append(args, "--namespace", namespace) } + if kubeContext != "" { + args = append(args, "--kube-context", kubeContext) + } cmd := exec.Command(os.Getenv("HELM_BIN"), args...) out, err := outputWithRichError(cmd) if err != nil { @@ -190,6 +202,9 @@ func (d *diffCmd) template(isUpgrade bool) ([]byte, error) { if d.namespace != "" { flags = append(flags, "--namespace", d.namespace) } + if d.kubeContext != "" { + flags = append(flags, "--kube-context", d.kubeContext) + } if d.postRenderer != "" { flags = append(flags, "--post-renderer", d.postRenderer) } @@ -412,6 +427,12 @@ func (d *diffCmd) writeExistingValues(f *os.File, all bool) error { if all { args = append(args, "--all") } + if d.namespace != "" { + args = append(args, "--namespace", d.namespace) + } + if d.kubeContext != "" { + args = append(args, "--kube-context", d.kubeContext) + } cmd := exec.Command(os.Getenv("HELM_BIN"), args...) debugPrint("Executing %s", strings.Join(cmd.Args, " ")) defer func() { diff --git a/cmd/release.go b/cmd/release.go index f0286e58..ebb00a0d 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -13,6 +13,7 @@ import ( ) type release struct { + kubeContext string detailedExitCode bool releases []string includeTests bool @@ -63,6 +64,7 @@ func releaseCmd() *cobra.Command { releaseCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") releaseCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") releaseCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + releaseCmd.Flags().StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use") AddDiffOptions(releaseCmd.Flags(), &diff.Options) releaseCmd.SuggestionsMinimumDistance = 1 @@ -82,11 +84,11 @@ func (d *release) differentiateHelm3() error { namespace1 = strings.Split(release1, "/")[0] release1 = strings.Split(release1, "/")[1] } - releaseResponse1, err := getRelease(release1, namespace1) + releaseResponse1, err := getRelease(release1, namespace1, d.kubeContext) if err != nil { return err } - releaseChart1, err := getChart(release1, namespace1) + releaseChart1, err := getChart(release1, namespace1, d.kubeContext) if err != nil { return err } @@ -97,11 +99,11 @@ func (d *release) differentiateHelm3() error { namespace2 = strings.Split(release2, "/")[0] release2 = strings.Split(release2, "/")[1] } - releaseResponse2, err := getRelease(release2, namespace2) + releaseResponse2, err := getRelease(release2, namespace2, d.kubeContext) if err != nil { return err } - releaseChart2, err := getChart(release2, namespace2) + releaseChart2, err := getChart(release2, namespace2, d.kubeContext) if err != nil { return err } diff --git a/cmd/revision.go b/cmd/revision.go index 8599789b..929a66d7 100644 --- a/cmd/revision.go +++ b/cmd/revision.go @@ -14,6 +14,7 @@ import ( type revision struct { release string + kubeContext string detailedExitCode bool revisions []string includeTests bool @@ -70,6 +71,7 @@ func revisionCmd() *cobra.Command { revisionCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") revisionCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") revisionCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + revisionCmd.Flags().StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use") AddDiffOptions(revisionCmd.Flags(), &diff.Options) revisionCmd.SuggestionsMinimumDistance = 1 @@ -85,14 +87,14 @@ func (d *revision) differentiateHelm3() error { } switch len(d.revisions) { case 1: - releaseResponse, err := getRelease(d.release, namespace) + releaseResponse, err := getRelease(d.release, namespace, d.kubeContext) if err != nil { return err } revision, _ := strconv.Atoi(d.revisions[0]) - revisionResponse, err := getRevision(d.release, revision, namespace) + revisionResponse, err := getRevision(d.release, revision, namespace, d.kubeContext) if err != nil { return err } @@ -110,12 +112,12 @@ func (d *revision) differentiateHelm3() error { revision1, revision2 = revision2, revision1 } - revisionResponse1, err := getRevision(d.release, revision1, namespace) + revisionResponse1, err := getRevision(d.release, revision1, namespace, d.kubeContext) if err != nil { return err } - revisionResponse2, err := getRevision(d.release, revision2, namespace) + revisionResponse2, err := getRevision(d.release, revision2, namespace, d.kubeContext) if err != nil { return err } diff --git a/cmd/rollback.go b/cmd/rollback.go index 6f8d6d17..4e70ba1e 100644 --- a/cmd/rollback.go +++ b/cmd/rollback.go @@ -14,6 +14,7 @@ import ( type rollback struct { release string + kubeContext string detailedExitCode bool revisions []string includeTests bool @@ -60,6 +61,7 @@ func rollbackCmd() *cobra.Command { rollbackCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") rollbackCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks") rollbackCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output") + rollbackCmd.Flags().StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use") AddDiffOptions(rollbackCmd.Flags(), &diff.Options) rollbackCmd.SuggestionsMinimumDistance = 1 @@ -74,7 +76,7 @@ func (d *rollback) backcastHelm3() error { excludes = []string{} } // get manifest of the latest release - releaseResponse, err := getRelease(d.release, namespace) + releaseResponse, err := getRelease(d.release, namespace, d.kubeContext) if err != nil { return err @@ -82,7 +84,7 @@ func (d *rollback) backcastHelm3() error { // get manifest of the release to rollback revision, _ := strconv.Atoi(d.revisions[0]) - revisionResponse, err := getRevision(d.release, revision, namespace) + revisionResponse, err := getRevision(d.release, revision, namespace, d.kubeContext) if err != nil { return err } diff --git a/cmd/upgrade.go b/cmd/upgrade.go index 40d01b4c..ec081c14 100644 --- a/cmd/upgrade.go +++ b/cmd/upgrade.go @@ -72,7 +72,8 @@ type diffCmd struct { // - "server": dry run is performed with remote cluster access // - "true": same as "client" // - "false": same as "none" - dryRunMode string + dryRunMode string + kubeContext string } func (d *diffCmd) isAllowUnreleased() bool { @@ -211,7 +212,7 @@ func newChartCommand() *cobra.Command { var kubeconfig string f.StringVar(&kubeconfig, "kubeconfig", "", "This flag is ignored, to allow passing of this top level flag to helm") f.BoolVar(&diff.threeWayMerge, "three-way-merge", false, "use three-way-merge to compute patch and generate diff output") - // f.StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use") + f.StringVar(&diff.kubeContext, "kube-context", "", "name of the kubeconfig context to use") f.StringVar(&diff.chartVersion, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used") f.StringVar(&diff.chartRepo, "repo", "", "specify the chart repository url to locate the requested chart") f.BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes") @@ -270,7 +271,7 @@ func (d *diffCmd) runHelm3() error { } if d.clusterAccessAllowed() { - releaseManifest, err = getRelease(d.release, d.namespace) + releaseManifest, err = getRelease(d.release, d.namespace, d.kubeContext) } var newInstall bool @@ -295,6 +296,9 @@ func (d *diffCmd) runHelm3() error { var actionConfig *action.Configuration if d.threeWayMerge || d.takeOwnership { actionConfig = new(action.Configuration) + if d.kubeContext != "" { + envSettings.KubeContext = d.kubeContext + } if err := actionConfig.Init(envSettings.RESTClientGetter(), envSettings.Namespace(), os.Getenv("HELM_DRIVER")); err != nil { log.Fatalf("%+v", err) } @@ -313,7 +317,7 @@ func (d *diffCmd) runHelm3() error { currentSpecs := make(map[string]*manifest.MappingResult) if !newInstall && d.clusterAccessAllowed() { if !d.noHooks && !d.threeWayMerge { - hooks, err := getHooks(d.release, d.namespace) + hooks, err := getHooks(d.release, d.namespace, d.kubeContext) if err != nil { return err } diff --git a/main_test.go b/main_test.go index 89445916..9943dbde 100644 --- a/main_test.go +++ b/main_test.go @@ -33,10 +33,118 @@ func TestHelmDiff(t *testing.T) { } }() + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + os.Args = []string{"helm-diff", "upgrade", "-f", "test/testdata/test-values.yaml", "test-release", "test/testdata/test-chart"} require.NoError(t, cmd.New().Execute()) } +func TestHelmDiffWithKubeContext(t *testing.T) { + os.Setenv(env, envValue) + defer os.Unsetenv(env) + + helmBin, helmBinSet := os.LookupEnv("HELM_BIN") + os.Setenv("HELM_BIN", os.Args[0]) + defer func() { + if helmBinSet { + os.Setenv("HELM_BIN", helmBin) + } else { + os.Unsetenv("HELM_BIN") + } + }() + + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"helm-diff", "upgrade", "-f", "test/testdata/test-values.yaml", "--kube-context", "test-context", "test-release", "test/testdata/test-chart"} + require.NoError(t, cmd.New().Execute()) +} + +func TestHelmDiffWithKubeContextReuseValues(t *testing.T) { + os.Setenv(env, envValue) + defer os.Unsetenv(env) + + helmBin, helmBinSet := os.LookupEnv("HELM_BIN") + os.Setenv("HELM_BIN", os.Args[0]) + defer func() { + if helmBinSet { + os.Setenv("HELM_BIN", helmBin) + } else { + os.Unsetenv("HELM_BIN") + } + }() + + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"helm-diff", "upgrade", "--reuse-values", "--kube-context", "test-context", "-f", "test/testdata/test-values.yaml", "test-release", "test/testdata/test-chart"} + require.NoError(t, cmd.New().Execute()) +} + +func TestHelmDiffRevisionWithKubeContext(t *testing.T) { + os.Setenv(env, envValue) + defer os.Unsetenv(env) + + helmBin, helmBinSet := os.LookupEnv("HELM_BIN") + os.Setenv("HELM_BIN", os.Args[0]) + defer func() { + if helmBinSet { + os.Setenv("HELM_BIN", helmBin) + } else { + os.Unsetenv("HELM_BIN") + } + }() + + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"helm-diff", "revision", "--kube-context", "test-context", "test-release", "2"} + require.NoError(t, cmd.New().Execute()) +} + +func TestHelmDiffRollbackWithKubeContext(t *testing.T) { + os.Setenv(env, envValue) + defer os.Unsetenv(env) + + helmBin, helmBinSet := os.LookupEnv("HELM_BIN") + os.Setenv("HELM_BIN", os.Args[0]) + defer func() { + if helmBinSet { + os.Setenv("HELM_BIN", helmBin) + } else { + os.Unsetenv("HELM_BIN") + } + }() + + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"helm-diff", "rollback", "--kube-context", "test-context", "test-release", "2"} + require.NoError(t, cmd.New().Execute()) +} + +func TestHelmDiffReleaseWithKubeContext(t *testing.T) { + os.Setenv(env, envValue) + defer os.Unsetenv(env) + + helmBin, helmBinSet := os.LookupEnv("HELM_BIN") + os.Setenv("HELM_BIN", os.Args[0]) + defer func() { + if helmBinSet { + os.Setenv("HELM_BIN", helmBin) + } else { + os.Unsetenv("HELM_BIN") + } + }() + + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + os.Args = []string{"helm-diff", "release", "--kube-context", "test-context", "test-release1", "test-release2"} + require.NoError(t, cmd.New().Execute()) +} + const ( env = "BECOME_FAKE_HELM" envValue = "1" @@ -70,6 +178,64 @@ var helmSubcmdStubs = []fakeHelmSubcmd{ cmd: []string{"get", "hooks"}, args: []string{"test-release"}, }, + { + cmd: []string{"get", "manifest"}, + args: []string{"test-release", "--kube-context", "test-context"}, + stdout: `--- +# Source: test-chart/templates/cm.yaml +`, + }, + { + cmd: []string{"template"}, + args: []string{"test-release", "test/testdata/test-chart", "--kube-context", "test-context", "--values", "test/testdata/test-values.yaml", "--validate", "--is-upgrade"}, + }, + { + cmd: []string{"get", "hooks"}, + args: []string{"test-release", "--kube-context", "test-context"}, + }, + { + cmd: []string{"get", "values"}, + args: []string{"test-release", "--output", "yaml", "--all", "--kube-context", "test-context"}, + }, + { + cmd: []string{"get", "values"}, + args: []string{"test-release", "--output", "yaml", "--all", "--kube-context", "test-context", "--namespace", "*"}, + }, + { + cmd: []string{"template"}, + args: []string{"test-release", "test/testdata/test-chart", "--kube-context", "test-context", "--values", "*", "--values", "test/testdata/test-values.yaml", "--validate", "--is-upgrade"}, + }, + { + cmd: []string{"get", "manifest"}, + args: []string{"test-release", "--revision", "2", "--kube-context", "test-context"}, + stdout: `--- +# Source: test-chart/templates/cm.yaml +`, + }, + { + cmd: []string{"get", "manifest"}, + args: []string{"test-release1", "--kube-context", "test-context"}, + stdout: `--- +# Source: test-chart/templates/cm.yaml +`, + }, + { + cmd: []string{"get", "all"}, + args: []string{"test-release1", "--template", "*", "--kube-context", "test-context"}, + stdout: `test-chart`, + }, + { + cmd: []string{"get", "manifest"}, + args: []string{"test-release2", "--kube-context", "test-context"}, + stdout: `--- +# Source: test-chart/templates/cm.yaml +`, + }, + { + cmd: []string{"get", "all"}, + args: []string{"test-release2", "--template", "*", "--kube-context", "test-context"}, + stdout: `test-chart`, + }, } func runFakeHelm() int { @@ -84,8 +250,15 @@ func runFakeHelm() int { for i := range helmSubcmdStubs { s := helmSubcmdStubs[i] if reflect.DeepEqual(s.cmd, cmdAndArgs[:len(s.cmd)]) { - stub = &s - break + want := s.args + if want == nil { + want = []string{} + } + got := cmdAndArgs[len(s.cmd):] + if argsMatch(want, got) { + stub = &s + break + } } } @@ -94,19 +267,22 @@ func runFakeHelm() int { return 1 } - want := stub.args - if want == nil { - want = []string{} - } - got := cmdAndArgs[len(stub.cmd):] - if !reflect.DeepEqual(want, got) { - _, _ = fmt.Fprintf(os.Stderr, "want: %v\n", want) - _, _ = fmt.Fprintf(os.Stderr, "got : %v\n", got) - _, _ = fmt.Fprintf(os.Stderr, "args : %v\n", os.Args) - _, _ = fmt.Fprintf(os.Stderr, "env : %v\n", os.Environ()) - return 1 - } _, _ = fmt.Fprintf(os.Stdout, "%s", stub.stdout) _, _ = fmt.Fprintf(os.Stderr, "%s", stub.stderr) return stub.exitCode } + +func argsMatch(want, got []string) bool { + if len(want) != len(got) { + return false + } + for i := range want { + if want[i] == "*" { + continue + } + if want[i] != got[i] { + return false + } + } + return true +}