diff --git a/cmd/helm/completion_test.go b/cmd/helm/completion_test.go index 1143d644516..3c9a9bab5e5 100644 --- a/cmd/helm/completion_test.go +++ b/cmd/helm/completion_test.go @@ -41,7 +41,7 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { }) testcmd := fmt.Sprintf("__complete %s ''", cmdName) - _, out, err := executeActionCommandC(storage, testcmd) + _, out, err := executeActionCommandC(storage, testcmd, "") if err != nil { t.Errorf("unexpected error, %s", err) } diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 1a22d058fd0..3c499736d85 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -36,7 +36,7 @@ func TestCreateCmd(t *testing.T) { defer testChdir(t, dir)() // Run a create - if _, _, err := executeActionCommand("create " + cname); err != nil { + if _, _, err := executeActionCommand("create "+cname, ""); err != nil { t.Fatalf("Failed to run create: %s", err) } @@ -81,7 +81,7 @@ func TestCreateStarterCmd(t *testing.T) { } // Run a create - if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=starterchart %s", cname)); err != nil { + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=starterchart %s", cname), ""); err != nil { t.Errorf("Failed to run create: %s", err) return } @@ -149,7 +149,7 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) { starterChartPath := filepath.Join(starterchart, "starterchart") // Run a create - if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=%s %s", starterChartPath, cname)); err != nil { + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=%s %s", starterChartPath, cname), ""); err != nil { t.Errorf("Failed to run create: %s", err) return } diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 37e3242c49f..f8605c84e54 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -59,7 +59,7 @@ func TestDependencyBuildCmd(t *testing.T) { repoFile := filepath.Join(rootDir, "repositories.yaml") cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) - _, out, err := executeActionCommand(cmd) + _, out, err := executeActionCommand(cmd, "") // In the first pass, we basically want the same results as an update. if err != nil { @@ -87,7 +87,7 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - _, out, err = executeActionCommand(cmd) + _, out, err = executeActionCommand(cmd, "") if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -118,7 +118,7 @@ func TestDependencyBuildCmd(t *testing.T) { } skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) - _, out, err = executeActionCommand(skipRefreshCmd) + _, out, err = executeActionCommand(skipRefreshCmd, "") // In this pass, we check --skip-refresh option becomes effective. if err != nil { @@ -139,7 +139,7 @@ func TestDependencyBuildCmd(t *testing.T) { dir("repositories.yaml"), dir(), dir()) - _, out, err = executeActionCommand(cmd) + _, out, err = executeActionCommand(cmd, "") if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -154,7 +154,7 @@ func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) { chartName := "testdata/testcharts/issue-7233" cmd := fmt.Sprintf("dependency build '%s'", chartName) - _, out, err := executeActionCommand(cmd) + _, out, err := executeActionCommand(cmd, "") // Want to make sure the build can verify Helm v2 hash if err != nil { diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index 967786b9abf..419f1b44a69 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -67,7 +67,7 @@ func TestDependencyUpdateCmd(t *testing.T) { } _, out, err := executeActionCommand( - fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), + fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), "", ) if err != nil { t.Logf("Output: %s", out) @@ -110,7 +110,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), "") if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -136,7 +136,7 @@ func TestDependencyUpdateCmd(t *testing.T) { dir("repositories.yaml"), dir(), dir()) - _, out, err = executeActionCommand(cmd) + _, out, err = executeActionCommand(cmd, "") if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -169,7 +169,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { } createTestingChart(t, dir(), chartname, srv.URL()) - _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), "") if err != nil { t.Logf("Output: %s", output) t.Fatal(err) @@ -178,7 +178,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { // Chart repo is down srv.Stop() - _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), "") if err == nil { t.Logf("Output: %s", output) t.Fatal("Expected error, got nil") @@ -232,6 +232,7 @@ func TestDependencyUpdateCmd_WithRepoThatWasNotAdded(t *testing.T) { _, out, err := executeActionCommand( fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), + "", ) if err != nil { diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 727cdaf88e5..cf72f421094 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -45,6 +45,7 @@ func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } cmd.AddCommand(newGetAllCmd(cfg, out)) + cmd.AddCommand(newGetDeployedCmd(cfg, out)) cmd.AddCommand(newGetValuesCmd(cfg, out)) cmd.AddCommand(newGetManifestCmd(cfg, out)) cmd.AddCommand(newGetHooksCmd(cfg, out)) diff --git a/cmd/helm/get_deployed.go b/cmd/helm/get_deployed.go new file mode 100644 index 00000000000..9d46127d101 --- /dev/null +++ b/cmd/helm/get_deployed.go @@ -0,0 +1,72 @@ +/* +Copyright The Helm 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 ( + "io" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" +) + +var getDeployedHelp = ` +This command prints list of resources deployed under a release. +` + +// newGetDeployedCmd creates a command for listing the resources deployed under a named release +func newGetDeployedCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + // Output format for the command output. This will be set by input flag -o (or --output). + var outfmt output.Format + + // Create get-deployed action's client + client := action.NewGetDeployed(cfg) + + cmd := &cobra.Command{ + Use: "deployed RELEASE_NAME", + Short: "list resources deployed under a named release", + Long: getDeployedHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + // Run the client to list resources under the release + resourceList, err := client.Run(args[0]) + if err != nil { + return err + } + + // Create an output writer with resources listed + writer := action.NewResourceListWriter(resourceList, false) + + // Write the resources list with output format provided with input flag + return outfmt.Write(out, writer) + }, + } + + // Add flag for specifying the output format + bindOutputFlag(cmd, &outfmt) + + return cmd +} diff --git a/cmd/helm/get_deployed_test.go b/cmd/helm/get_deployed_test.go new file mode 100644 index 00000000000..e6568923d8c --- /dev/null +++ b/cmd/helm/get_deployed_test.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm 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" + + "helm.sh/helm/v3/pkg/release" +) + +func TestGetDeployed(t *testing.T) { + tests := []cmdTestCase{ + { + name: "get deployed with a release", + cmd: "get deployed thomas-guide", + golden: "output/get-deployed.txt", + rels: []*release.Release{ + release.Mock( + &release.MockReleaseOptions{ + Name: "thomas-guide", + }, + ), + }, + namespace: "default", + }, + } + + runTestCmd(t, tests) +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index b20b1a24de3..5ac12c8c057 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -59,7 +59,7 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { } } t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) - _, out, err := executeActionCommandC(storage, tt.cmd) + _, out, err := executeActionCommandC(storage, tt.cmd, tt.namespace) if tt.wantError && err == nil { t.Errorf("expected error, got success with the following output:\n%s", out) } @@ -78,11 +78,11 @@ func storageFixture() *storage.Storage { return storage.Init(driver.NewMemory()) } -func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { - return executeActionCommandStdinC(store, nil, cmd) +func executeActionCommandC(store *storage.Storage, cmd, ns string) (*cobra.Command, string, error) { + return executeActionCommandStdinC(store, nil, cmd, ns) } -func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { +func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd, ns string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err @@ -91,10 +91,14 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) buf := new(bytes.Buffer) actionConfig := &action.Configuration{ - Releases: store, - KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, - Capabilities: chartutil.DefaultCapabilities, - Log: func(format string, v ...interface{}) {}, + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{ + Out: io.Discard, + Namespace: ns, + }, + Capabilities: chartutil.DefaultCapabilities, + Log: func(format string, v ...interface{}) {}, + RESTClientGetter: settings.RESTClientGetter(), } root, err := newRootCmd(actionConfig, buf, args) @@ -135,10 +139,12 @@ type cmdTestCase struct { // Number of repeats (in case a feature was previously flaky and the test checks // it's now stably producing identical results). 0 means test is run exactly once. repeat int + // namespace is the default namespace set in kube client + namespace string } -func executeActionCommand(cmd string) (*cobra.Command, string, error) { - return executeActionCommandC(storageFixture(), cmd) +func executeActionCommand(cmd, ns string) (*cobra.Command, string, error) { + return executeActionCommandC(storageFixture(), cmd, ns) } func resetEnv() func() { diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index 9093b510cfd..9d22c71890c 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -138,7 +138,7 @@ func TestPackage(t *testing.T) { cmd = append(cmd, fmt.Sprintf("--%s=%s", k, v)) } } - _, _, err = executeActionCommand(strings.Join(cmd, " ")) + _, _, err = executeActionCommand(strings.Join(cmd, " "), "") if err != nil { if tt.err && re.MatchString(err.Error()) { return @@ -171,7 +171,7 @@ func TestSetAppVersion(t *testing.T) { chartToPackage := "testdata/testcharts/alpine" dir := t.TempDir() cmd := fmt.Sprintf("package %s --destination=%s --app-version=%s", chartToPackage, dir, expectedAppVersion) - _, output, err := executeActionCommand(cmd) + _, output, err := executeActionCommand(cmd, "") if err != nil { t.Logf("Output: %s", output) t.Fatal(err) diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index 41ac237f4ae..2cdbf913be7 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -220,7 +220,7 @@ func TestPullCmd(t *testing.T) { t.Fatal(err) } } - _, out, err := executeActionCommand(cmd) + _, out, err := executeActionCommand(cmd, "") if err != nil { if tt.wantError { if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { @@ -337,7 +337,7 @@ func TestPullWithCredentialsCmd(t *testing.T) { t.Fatal(err) } } - _, _, err := executeActionCommand(cmd) + _, _, err := executeActionCommand(cmd, "") if err != nil { if tt.wantError { if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 2386bb01fa0..60613f984c8 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -262,7 +262,7 @@ func TestRepoAddWithPasswordFromStdin(t *testing.T) { const username = "username" cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username) var result string - _, result, err = executeActionCommandStdinC(store, in, cmd) + _, result, err = executeActionCommandStdinC(store, in, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index b58e4c162f3..d9ec7669e4b 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -151,7 +151,7 @@ func TestRollbackWithLabels(t *testing.T) { t.Fatal(err) } } - _, _, err := executeActionCommandC(storage, fmt.Sprintf("rollback %s 1", releaseName)) + _, _, err := executeActionCommandC(storage, fmt.Sprintf("rollback %s 1", releaseName), "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go index 65e6d66c711..de6de1086bc 100644 --- a/cmd/helm/root_test.go +++ b/cmd/helm/root_test.go @@ -83,7 +83,7 @@ func TestRootCmd(t *testing.T) { os.Setenv(k, v) } - if _, _, err := executeActionCommand(tt.args); err != nil { + if _, _, err := executeActionCommand(tt.args, ""); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -115,7 +115,7 @@ func TestRootCmd(t *testing.T) { } func TestUnknownSubCmd(t *testing.T) { - _, _, err := executeActionCommand("foobar") + _, _, err := executeActionCommand("foobar", "") if err == nil || err.Error() != `unknown command "foobar" for "helm"` { t.Errorf("Expect unknown command error, got %q", err) diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go index 89ce2b3e53a..1f2b933a8cc 100644 --- a/cmd/helm/search_hub_test.go +++ b/cmd/helm/search_hub_test.go @@ -42,7 +42,7 @@ func TestSearchHubCmd(t *testing.T) { testcmd := "search hub --endpoint " + ts.URL + " maria" storage := storageFixture() - _, out, err := executeActionCommandC(storage, testcmd) + _, out, err := executeActionCommandC(storage, testcmd, "") if err != nil { t.Errorf("unexpected error, %s", err) } @@ -72,7 +72,7 @@ func TestSearchHubListRepoCmd(t *testing.T) { testcmd := "search hub --list-repo-url --endpoint " + ts.URL + " maria" storage := storageFixture() - _, out, err := executeActionCommandC(storage, testcmd) + _, out, err := executeActionCommandC(storage, testcmd, "") if err != nil { t.Errorf("unexpected error, %s", err) } @@ -165,7 +165,7 @@ func TestSearchHubCmd_FailOnNoResponseTests(t *testing.T) { storage := storageFixture() - _, out, err := executeActionCommandC(storage, tt.cmd) + _, out, err := executeActionCommandC(storage, tt.cmd, "") if tt.wantErr { if err == nil { t.Errorf("expected error due to no record in response, got nil") diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go index 93ec08d0f6b..91d7de3dfe2 100644 --- a/cmd/helm/show_test.go +++ b/cmd/helm/show_test.go @@ -74,7 +74,7 @@ func TestShowPreReleaseChart(t *testing.T) { outdir, ) //_, out, err := executeActionCommand(cmd) - _, _, err := executeActionCommand(cmd) + _, _, err := executeActionCommand(cmd, "") if err != nil { if tt.fail { if !strings.Contains(err.Error(), tt.expectedErr) { diff --git a/cmd/helm/testdata/output/get-deployed.txt b/cmd/helm/testdata/output/get-deployed.txt new file mode 100644 index 00000000000..700ac72e844 --- /dev/null +++ b/cmd/helm/testdata/output/get-deployed.txt @@ -0,0 +1,2 @@ +NAMESPACE NAME API_VERSION AGE +default secrets/fixture v1 0s diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 485267d1d6d..9059609b3b9 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -190,7 +190,7 @@ func TestUpgradeWithValue(t *testing.T) { store.Create(relMock(releaseName, 3, ch)) cmd := fmt.Sprintf("upgrade %s --set favoriteDrink=tea '%s'", releaseName, chartPath) - _, _, err := executeActionCommandC(store, cmd) + _, _, err := executeActionCommandC(store, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -217,7 +217,7 @@ func TestUpgradeWithStringValue(t *testing.T) { store.Create(relMock(releaseName, 3, ch)) cmd := fmt.Sprintf("upgrade %s --set-string favoriteDrink=coffee '%s'", releaseName, chartPath) - _, _, err := executeActionCommandC(store, cmd) + _, _, err := executeActionCommandC(store, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -245,7 +245,7 @@ func TestUpgradeInstallWithSubchartNotes(t *testing.T) { store.Create(relMock(releaseName, 1, ch)) cmd := fmt.Sprintf("upgrade %s -i --render-subchart-notes '%s'", releaseName, "testdata/testcharts/chart-with-subchart-notes") - _, _, err := executeActionCommandC(store, cmd) + _, _, err := executeActionCommandC(store, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -277,7 +277,7 @@ func TestUpgradeWithValuesFile(t *testing.T) { store.Create(relMock(releaseName, 3, ch)) cmd := fmt.Sprintf("upgrade %s --values testdata/testcharts/upgradetest/values.yaml '%s'", releaseName, chartPath) - _, _, err := executeActionCommandC(store, cmd) + _, _, err := executeActionCommandC(store, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -310,7 +310,7 @@ func TestUpgradeWithValuesFromStdin(t *testing.T) { } cmd := fmt.Sprintf("upgrade %s --values - '%s'", releaseName, chartPath) - _, _, err = executeActionCommandStdinC(store, in, cmd) + _, _, err = executeActionCommandStdinC(store, in, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -340,7 +340,7 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { } cmd := fmt.Sprintf("upgrade %s -f - --install '%s'", releaseName, chartPath) - _, _, err = executeActionCommandStdinC(store, in, cmd) + _, _, err = executeActionCommandStdinC(store, in, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -444,7 +444,7 @@ func TestUpgradeInstallWithLabels(t *testing.T) { "key2": "val2", } cmd := fmt.Sprintf("upgrade %s --install --labels key1=val1,key2=val2 '%s'", releaseName, chartPath) - _, _, err := executeActionCommandC(store, cmd) + _, _, err := executeActionCommandC(store, cmd, "") if err != nil { t.Errorf("unexpected error, got '%v'", err) } diff --git a/cmd/helm/verify_test.go b/cmd/helm/verify_test.go index 23b7935577c..3984278fd61 100644 --- a/cmd/helm/verify_test.go +++ b/cmd/helm/verify_test.go @@ -72,7 +72,7 @@ func TestVerifyCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, out, err := executeActionCommand(tt.cmd) + _, out, err := executeActionCommand(tt.cmd, "") if tt.wantError { if err == nil { t.Errorf("Expected error, but got none: %q", out) diff --git a/go.mod b/go.mod index e200d4fcb11..61c746c3554 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,8 @@ require ( k8s.io/klog/v2 v2.110.1 k8s.io/kubectl v0.29.0 oras.land/oras-go v1.2.4 + sigs.k8s.io/controller-runtime v0.16.5 + sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 sigs.k8s.io/yaml v1.3.0 ) @@ -164,6 +166,5 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect - sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index 2799262df39..e0e74b7b200 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= @@ -125,6 +127,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -416,6 +420,10 @@ go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1 go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= @@ -586,6 +594,8 @@ k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSn k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= +sigs.k8s.io/controller-runtime v0.16.5 h1:yr1cEJbX08xsTW6XEIzT13KHHmIyX8Umvme2cULvFZw= +sigs.k8s.io/controller-runtime v0.16.5/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= diff --git a/pkg/action/get_deployed.go b/pkg/action/get_deployed.go new file mode 100644 index 00000000000..43b63abda7c --- /dev/null +++ b/pkg/action/get_deployed.go @@ -0,0 +1,297 @@ +/* +Copyright The Helm 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 action + +import ( + "context" + "fmt" + "io" + "strings" + + "helm.sh/helm/v3/pkg/cli/output" + + "github.com/gosuri/uitable" + "k8s.io/apimachinery/pkg/api/meta" + metatable "k8s.io/apimachinery/pkg/api/meta/table" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// getDeployed is the action for checking the named release's deployed resource list. It is the implementation +// of 'helm get deployed' subcommand. +// +// For eg. say there is an nginx release with just a deployment and a service, this will list as follows: +// +// $ helm get deployed nginx +// NAMESPACE NAME API_VERSION AGE +// default services/nginx v1 38s +// default deployments/nginx apps/v1 38s +type getDeployed struct { + cfg *Configuration +} + +// NewGetDeployed creates a new GetDeployed object with the input configuration. +func NewGetDeployed(cfg *Configuration) *getDeployed { + return &getDeployed{ + cfg: cfg, + } +} + +// Run executes 'helm get deployed' against the named release. +func (g *getDeployed) Run(name string) ([]resourceElement, error) { + ctx := context.Background() + + // Check if cluster is reachable from the client + if err := g.cfg.KubeClient.IsReachable(); err != nil { + return nil, fmt.Errorf("kube client is not reachable: %w", err) + } + + // Load the REST config for client + config, err := g.cfg.RESTClientGetter.ToRESTConfig() + if err != nil { + return nil, fmt.Errorf("failed to get the REST config: %w", err) + } + + // Create a dynamic client + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create a dynamic client: %w", err) + } + + // Create a REST mapper from config + restMapper, err := g.cfg.RESTClientGetter.ToRESTMapper() + if err != nil { + return nil, fmt.Errorf("failed to get the REST mapper: %w", err) + } + + // Default namespace set in kube client + defaultNamespace := g.cfg.KubeClient.GetNamespace() + + // Get the release details. The revision is set to 0 to get the latest revision of the release. + release, err := g.cfg.releaseContent(name, 0) + if err != nil { + return nil, fmt.Errorf("failed to fetch release content: %w", err) + } + + // Create function to iterate over all the resources in the release manifest + resourceList := make([]resourceElement, 0) + listResourcesFn := kio.FilterFunc(func(resources []*yaml.RNode) ([]*yaml.RNode, error) { + // Iterate over the resource in manifest YAML + for _, manifest := range resources { + // Process resource to be printable by the "helm get deployed" command's output writer + resource, err := processResourceForGetDeployed(ctx, manifest, dynamicClient, restMapper, defaultNamespace) + if err != nil { + return nil, err + } + + resourceList = append(resourceList, *resource) + } + + // The current command shouldn't alter the list of resources. Hence returning resources list as it. + return resources, nil + }) + + // Run the manifest YAML through the function to process the resources list + err = kio.Pipeline{ + Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(release.Manifest)}}, + Filters: []kio.Filter{listResourcesFn}, + }.Execute() + if err != nil { + return nil, fmt.Errorf("failed to process release manifests: %w", err) + } + + return resourceList, nil +} + +// processResourceForGetDeployed processes resource to be printable by the "helm get deployed" command's output writer +func processResourceForGetDeployed(ctx context.Context, manifest *yaml.RNode, dynamicClient *dynamic.DynamicClient, + restMapper meta.RESTMapper, defaultNamespace string) (*resourceElement, error) { + // Extract the resource's name field from manifest YAML + name := manifest.GetName() + if name == "" { + return nil, fmt.Errorf("resource name not found in manifest: %v", manifest) + } + + // Extract the resource's API version field from manifest YAML + apiVersion := manifest.GetApiVersion() + if apiVersion == "" { + return nil, fmt.Errorf("resource api version not found in manifest: %v", manifest) + } + + // Extract the resource's GVK + gvk, err := extractGVK(manifest) + if err != nil { + return nil, err + } + + // Extract the resource's namespace + namespace, err := extractResourceNamespace(manifest, *gvk, restMapper, defaultNamespace) + if err != nil { + return nil, err + } + + // Create a REST mapping for the resource and GVK + restMappingForGVK, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return nil, fmt.Errorf("failed to get the REST mapping for GVK: %w", err) + } + + // Get the resource's details from the cluster + resource, err := getResource(ctx, dynamicClient, name, namespace, restMappingForGVK.Resource) + if err != nil { + return nil, err + } + + return &resourceElement{ + Resource: restMappingForGVK.Resource.Resource, + Name: name, + Namespace: namespace, + APIVersion: apiVersion, + CreationTimestamp: resource.GetCreationTimestamp(), + }, nil +} + +// getResource gets the Kubernetes resource using dynamic client. +func getResource(ctx context.Context, client *dynamic.DynamicClient, name, namespace string, + gvr schema.GroupVersionResource) (*unstructured.Unstructured, error) { + // If the namespace is not empty, it looks for a namespace-scoped resource. It is the responsibility of the caller + // to provide the namespace value for the namespace-scoped resource even if it uses the default namespace. + if namespace != "" { + resource, err := client.Resource(gvr). + Namespace(namespace). + Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get the namespaced resource %q: %w", gvr, err) + } + + return resource, nil + } + + // Get cluster-scoped resource + resource, err := client.Resource(gvr). + Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get the non-namespaced resource %q: %w", gvr, err) + } + + return resource, nil +} + +// extractGVK extracts group-version and kind from the manifest YAML, and forms a GVK out of it. +func extractGVK(manifest *yaml.RNode) (*schema.GroupVersionKind, error) { + // Get the group-version field from manifest YAML + gv := manifest.GetApiVersion() + if gv == "" { + return nil, fmt.Errorf("failed to get the resource's apiVersion from manifest: %v", manifest) + } + + // Get the kind field from manifest YAML + kind := manifest.GetKind() + if kind == "" { + return nil, fmt.Errorf("failed to get the resource's kind from manifest: %v", manifest) + } + + // Create GVK out of group-version and kind from manifest + gvk := schema.FromAPIVersionAndKind(gv, kind) + + return &gvk, nil +} + +// extractResourceNamespace extracts resource name field ("manifest.namespace") from the manifest YAML, if the +// resource is namespace-scoped. For cluster-scoped, it returns empty string. +// +// Note: The YAML RNode should be of a single resource. +func extractResourceNamespace(manifest *yaml.RNode, gvk schema.GroupVersionKind, restMapper meta.RESTMapper, + defaultNamespace string) (string, error) { + // Extract the resource's namespace field from manifest YAML + namespace := manifest.GetNamespace() + if namespace != "" { + return namespace, nil + } + + // Check whether the current resource is namespace-scoped + isResourceNamespaced, err := apiutil.IsGVKNamespaced(gvk, restMapper) + if err != nil { + return "", fmt.Errorf("failed to check if GVK is namespaced: %w", err) + } + + // When the resource is namespace-scoped, and namespace field is missing (or empty) in the manifest, use the + // default namespace. Note: The default namespace can be "default" or an overridden value in kube config. + if isResourceNamespaced { + return defaultNamespace, nil + } + + // When the resource is cluster-scoped, return empty string + return "", nil +} + +type resourceElement struct { + Name string `json:"name"` // Resource's name + Namespace string `json:"namespace"` // Resource's namespace + APIVersion string `json:"apiVersion"` // Resource's group-version + Resource string `json:"resource"` // Resource type (eg. pods, deployments, etc.) + CreationTimestamp metav1.Time `json:"creationTimestamp"` // Resource creation timestamp +} + +type resourceListWriter struct { + releases []resourceElement // Resources list + noHeaders bool // Toggle to disable headers in tabular format +} + +// NewResourceListWriter creates a output writer for Kubernetes resources to be listed with 'helm get deployed' +func NewResourceListWriter(resources []resourceElement, noHeaders bool) output.Writer { + return &resourceListWriter{resources, noHeaders} +} + +// WriteTable prints the resources list in a tabular format +func (r *resourceListWriter) WriteTable(out io.Writer) error { + // Create table writer + table := uitable.New() + + // Add headers if enabled + if !r.noHeaders { + table.AddRow("NAMESPACE", "NAME", "API_VERSION", "AGE") + } + + // Add resources to table + for _, r := range r.releases { + table.AddRow( + r.Namespace, // Namespace + fmt.Sprintf("%s/%s", r.Resource, r.Name), // Name + r.APIVersion, // API version + metatable.ConvertToHumanReadableDateType(r.CreationTimestamp), // Age + ) + } + + // Format the table and write to output writer + return output.EncodeTable(out, table) +} + +// WriteTable prints the resources list in a JSON format +func (r *resourceListWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, r.releases) +} + +// WriteTable prints the resources list in a YAML format +func (r *resourceListWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, r.releases) +} diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 9df833a434c..81ec1cbda35 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -320,7 +320,8 @@ func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) er return w.waitForDeletedResources(resources) } -func (c *Client) namespace() string { +// GetNamespace returns the namespace set in the client (in kube config), or if that is missing, it returns "default" +func (c *Client) GetNamespace() string { if c.Namespace != "" { return c.Namespace } @@ -334,7 +335,7 @@ func (c *Client) namespace() string { func (c *Client) newBuilder() *resource.Builder { return c.Factory.NewBuilder(). ContinueOnError(). - NamespaceParam(c.namespace()). + NamespaceParam(c.GetNamespace()). DefaultNamespace(). Flatten() } @@ -825,7 +826,7 @@ func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) return v1.PodUnknown, err } to := int64(timeout) - watcher, err := client.CoreV1().Pods(c.namespace()).Watch(context.Background(), metav1.ListOptions{ + watcher, err := client.CoreV1().Pods(c.GetNamespace()).Watch(context.Background(), metav1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", name), TimeoutSeconds: &to, }) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index cc2c84b40b8..c241b6c1e9c 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -32,7 +32,8 @@ import ( // PrintingKubeClient implements KubeClient, but simply prints the reader to // the given output. type PrintingKubeClient struct { - Out io.Writer + Out io.Writer + Namespace string } // IsReachable checks if the cluster is reachable @@ -127,6 +128,13 @@ func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.Resource return &kube.Result{Deleted: resources}, nil } +// GetNamespace returns the namespace set in the client. +// +// This is not required by the PrintingKubeClient, but to implement (pkg/kube).Interface +func (p *PrintingKubeClient) GetNamespace() string { + return p.Namespace +} + func bufferize(resources kube.ResourceList) io.Reader { var builder strings.Builder for _, info := range resources { diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index ce42ed9501d..5dfff95956d 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -70,6 +70,10 @@ type Interface interface { // IsReachable checks whether the client is able to connect to the cluster. IsReachable() error + + // GetNamespace returns the namespace set in the client (in kube config). + // Or if that is missing, it returns "default" + GetNamespace() string } // InterfaceExt is introduced to avoid breaking backwards compatibility for Interface implementers.