Repository: helm/helm (go)

Issue:
helm template: show-only does not show files in crds directory
<!-- If you need help or think you have found a bug, please help us with your issue by entering the following information (otherwise you can delete this text): -->
A helm chart provides an easy way to fetch CRDs in a specific version. I want to install CRDs from a helm chart without applying the rest of the manifests. Currently there are at least 3 workflows which I use to do this:
- Pull CRDs from github and apply them with `kubectl`
- Render the full chart, extract the CRDs and then apply them
- Use a chart that has CRDs in the `templates/` directory, in which case `--show-only` works, and apply them directly

I'd like to be able to use `--show-only` with files in the `crds/` directory.

An example:
```
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm template kube-prometheus-stack prometheus-community/kube-prometheus-stack --include-crds -s crds/*
```

Output of `helm version`:

`version.BuildInfo{Version:"v3.6.0", GitCommit:"7f2df6467771a75f5646b7f12afb408590ed1755", GitTreeState:"clean", GoVersion:"go1.16.3"}`

Output of `kubectl version`:

Cloud Provider/Platform (AKS, GKE, Minikube etc.): 




Pull request description:
Implement show crds subcommand
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

This subcommand will display manifests under `crds/` if some exist. This also changes the behaviour of `show all` to include CRDs.

Suggested by @mattfarina to fix #9785

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/cmd/helm/show.go b/cmd/helm/show.go
index 888d2d3f3..7c6da270c 100644
--- a/cmd/helm/show.go
+++ b/cmd/helm/show.go
@@ -51,6 +51,11 @@ This command inspects a chart (directory, file, or URL) and displays the content
 of the README file
 `
 
+const showCRDsDesc = `
+This command inspects a chart (directory, file, or URL) and displays the contents
+of the CustomResourceDefintion files
+`
+
 func newShowCmd(out io.Writer) *cobra.Command {
 	client := action.NewShow(action.ShowAll)
 
@@ -139,7 +144,24 @@ func newShowCmd(out io.Writer) *cobra.Command {
 		},
 	}
 
-	cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd}
+	crdsSubCmd := &cobra.Command{
+		Use:               "crds [CHART]",
+		Short:             "show the chart's CRDs",
+		Long:              showCRDsDesc,
+		Args:              require.ExactArgs(1),
+		ValidArgsFunction: validArgsFunc,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			client.OutputFormat = action.ShowCRDs
+			output, err := runShow(args, client)
+			if err != nil {
+				return err
+			}
+			fmt.Fprint(out, output)
+			return nil
+		},
+	}
+
+	cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd}
 	for _, subCmd := range cmds {
 		addShowFlags(subCmd, client)
 		showCommand.AddCommand(subCmd)
diff --git a/pkg/action/show.go b/pkg/action/show.go
index 4eab2b4fd..bd2e08f6a 100644
--- a/pkg/action/show.go
+++ b/pkg/action/show.go
@@ -17,6 +17,7 @@ limitations under the License.
 package action
 
 import (
+	"bytes"
 	"fmt"
 	"strings"
 
@@ -41,6 +42,8 @@ const (
 	ShowValues ShowOutputFormat = "values"
 	// ShowReadme is the format which only shows the chart's README
 	ShowReadme ShowOutputFormat = "readme"
+	// ShowCRDs is the format which only shows the chart's CRDs
+	ShowCRDs ShowOutputFormat = "crds"
 )
 
 var readmeFileNames = []string{"readme.md", "readme.txt", "readme"}
@@ -115,6 +118,18 @@ func (s *Show) Run(chartpath string) (string, error) {
 		}
 		fmt.Fprintf(&out, "%s\n", readme.Data)
 	}
+
+	if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
+		crds := s.chart.CRDObjects()
+		if len(crds) > 0 {
+			if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
+				fmt.Fprintln(&out, "---")
+			}
+			for _, crd := range crds {
+				fmt.Fprintf(&out, "%s\n", string(crd.File.Data))
+			}
+		}
+	}
 	return out.String(), nil
 }
 


Test patch:
diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go
index 9781a3de4..8dba0aea4 100644
--- a/cmd/helm/show_test.go
+++ b/cmd/helm/show_test.go
@@ -145,3 +145,7 @@ func TestShowReadmeFileCompletion(t *testing.T) {
 func TestShowValuesFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "show values", true)
 }
+
+func TestShowCRDsFileCompletion(t *testing.T) {
+	checkFileCompletion(t, "show crds", true)
+}
diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go
index a2efdc8ac..fda74caba 100644
--- a/pkg/action/show_test.go
+++ b/pkg/action/show_test.go
@@ -28,6 +28,9 @@ func TestShow(t *testing.T) {
 		Metadata: &chart.Metadata{Name: "alpine"},
 		Files: []*chart.File{
 			{Name: "README.md", Data: []byte("README\n")},
+			{Name: "crds/ignoreme.txt", Data: []byte("error")},
+			{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
+			{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
 		},
 		Raw: []*chart.File{
 			{Name: "values.yaml", Data: []byte("VALUES\n")},
@@ -48,6 +51,12 @@ VALUES
 ---
 README
 
+---
+foo
+
+---
+bar
+
 `
 	if output != expect {
 		t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
@@ -83,3 +92,31 @@ func TestShowValuesByJsonPathFormat(t *testing.T) {
 		t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
 	}
 }
+
+func TestShowCRDs(t *testing.T) {
+	client := NewShow(ShowCRDs)
+	client.chart = &chart.Chart{
+		Metadata: &chart.Metadata{Name: "alpine"},
+		Files: []*chart.File{
+			{Name: "crds/ignoreme.txt", Data: []byte("error")},
+			{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
+			{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
+		},
+	}
+
+	output, err := client.Run("")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expect := `---
+foo
+
+---
+bar
+
+`
+	if output != expect {
+		t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
+	}
+}