From dc7600551e66c441ce15600c85ba3145e54a6819 Mon Sep 17 00:00:00 2001 From: Warren Fernandes Date: Tue, 28 Jul 2020 17:02:09 -0600 Subject: [PATCH] Support reading from stdin --- cmd/clusterctl/client/config.go | 30 ++++++++++++++-- cmd/clusterctl/client/config_test.go | 35 +++++++++++++++++++ cmd/clusterctl/cmd/generate_yaml.go | 27 ++++++++++---- cmd/clusterctl/cmd/generate_yaml_test.go | 22 ++++++++++-- .../src/clusterctl/commands/generate-yaml.md | 10 ++++++ 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/cmd/clusterctl/client/config.go b/cmd/clusterctl/client/config.go index 67540becd7ce..19052a6ece5f 100644 --- a/cmd/clusterctl/client/config.go +++ b/cmd/clusterctl/client/config.go @@ -17,6 +17,8 @@ limitations under the License. package client import ( + "io" + "io/ioutil" "strconv" "k8s.io/utils/pointer" @@ -26,6 +28,7 @@ import ( clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" "sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository" + yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor" ) func (c *clusterctlClient) GetProvidersConfig() ([]Provider, error) { @@ -58,8 +61,15 @@ func (c *clusterctlClient) GetProviderComponents(provider string, providerType c return components, nil } +// ReaderSourceOptions define the options to be used when reading a template +// from an arbitrary reader +type ReaderSourceOptions struct { + Reader io.Reader +} + // ProcessYAMLOptions are the options supported by ProcessYAML. type ProcessYAMLOptions struct { + ReaderSource *ReaderSourceOptions // URLSource to be used for reading the template URLSource *URLSourceOptions @@ -69,6 +79,20 @@ type ProcessYAMLOptions struct { } func (c *clusterctlClient) ProcessYAML(options ProcessYAMLOptions) (YamlPrinter, error) { + if options.ReaderSource != nil { + content, err := ioutil.ReadAll(options.ReaderSource.Reader) + if err != nil { + return nil, err + } + return repository.NewTemplate(repository.TemplateInput{ + RawArtifact: content, + ConfigVariablesClient: c.configClient.Variables(), + Processor: yaml.NewSimpleProcessor(), + TargetNamespace: "", + ListVariablesOnly: options.ListVariablesOnly, + }) + } + // Technically we do not need to connect to the cluster. However, we are // leveraging the template client which exposes GetFromURL() is available // on the cluster client so we create a cluster client with default @@ -83,11 +107,11 @@ func (c *clusterctlClient) ProcessYAML(options ProcessYAMLOptions) (YamlPrinter, return nil, err } - if options.URLSource == nil { - return nil, errors.New("unable to read custom template. Please specify a template source") + if options.URLSource != nil { + return c.getTemplateFromURL(cluster, *options.URLSource, "", options.ListVariablesOnly) } - return c.getTemplateFromURL(cluster, *options.URLSource, "", options.ListVariablesOnly) + return nil, errors.New("unable to read custom template. Please specify a template source") } // GetClusterTemplateOptions carries the options supported by GetClusterTemplate. diff --git a/cmd/clusterctl/client/config_test.go b/cmd/clusterctl/client/config_test.go index d8a1034b8032..5b0c66d3a207 100644 --- a/cmd/clusterctl/client/config_test.go +++ b/cmd/clusterctl/client/config_test.go @@ -21,9 +21,11 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" . "github.com/onsi/gomega" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -611,6 +613,8 @@ v3: ${VAR3:-default3}` templateFile := filepath.Join(dir, "template.yaml") g.Expect(ioutil.WriteFile(templateFile, []byte(template), 0600)).To(Succeed()) + inputReader := strings.NewReader(template) + tests := []struct { name string options ProcessYAMLOptions @@ -649,6 +653,30 @@ v3: default3`, options: ProcessYAMLOptions{}, expectErr: true, }, + { + name: "processes yaml from specified reader", + options: ProcessYAMLOptions{ + ReaderSource: &ReaderSourceOptions{ + Reader: inputReader, + }, + ListVariablesOnly: false, + }, + expectErr: false, + expectedYaml: `v1: default1 +v2: default2 +v3: default3`, + expectedVars: []string{"VAR1", "VAR2", "VAR3"}, + }, + { + name: "returns error if unable to read from reader", + options: ProcessYAMLOptions{ + ReaderSource: &ReaderSourceOptions{ + Reader: &errReader{}, + }, + ListVariablesOnly: false, + }, + expectErr: true, + }, } for _, tt := range tests { @@ -676,3 +704,10 @@ v3: default3`, } } + +// errReader returns a non-EOF error on the first read. +type errReader struct{} + +func (e *errReader) Read(p []byte) (n int, err error) { + return 0, errors.New("read error") +} diff --git a/cmd/clusterctl/cmd/generate_yaml.go b/cmd/clusterctl/cmd/generate_yaml.go index b278ad308c25..039f92a71daa 100644 --- a/cmd/clusterctl/cmd/generate_yaml.go +++ b/cmd/clusterctl/cmd/generate_yaml.go @@ -51,17 +51,24 @@ var generateYamlCmd = &cobra.Command{ # Generates a configuration file with variable values using a template stored locally. - clusterctl generate yaml --from ~/workspace/cluster-template.yaml`), + clusterctl generate yaml --from ~/workspace/cluster-template.yaml + + # Prints list of variables used in the local template + clusterctl generate yaml --from ~/workspace/cluster-template.yaml --list-variables + + # Prints list of variables from template passed in via stdin + cat ~/workspace/cluster-template.yaml | clusterctl generate yaml --from - --list-variables +`), RunE: func(cmd *cobra.Command, args []string) error { - return generateYAML(os.Stdout) + return generateYAML(os.Stdin, os.Stdout) }, } func init() { // flags for the url source - generateYamlCmd.Flags().StringVar(&gyOpts.url, "from", "", - "The URL to read the template from.") + generateYamlCmd.Flags().StringVar(&gyOpts.url, "from", "-", + "The URL to read the template from. It defaults to '-' which reads from stdin.") // other flags generateYamlCmd.Flags().BoolVar(&gyOpts.listVariables, "list-variables", false, @@ -70,7 +77,7 @@ func init() { generateCmd.AddCommand(generateYamlCmd) } -func generateYAML(w io.Writer) error { +func generateYAML(r io.Reader, w io.Writer) error { c, err := client.New(cfgFile) if err != nil { return err @@ -79,8 +86,14 @@ func generateYAML(w io.Writer) error { ListVariablesOnly: gyOpts.listVariables, } if gyOpts.url != "" { - options.URLSource = &client.URLSourceOptions{ - URL: gyOpts.url, + if gyOpts.url == "-" { + options.ReaderSource = &client.ReaderSourceOptions{ + Reader: r, + } + } else { + options.URLSource = &client.URLSourceOptions{ + URL: gyOpts.url, + } } } printer, err := c.ProcessYAML(options) diff --git a/cmd/clusterctl/cmd/generate_yaml_test.go b/cmd/clusterctl/cmd/generate_yaml_test.go index f687129f3306..e8166751b2d1 100644 --- a/cmd/clusterctl/cmd/generate_yaml_test.go +++ b/cmd/clusterctl/cmd/generate_yaml_test.go @@ -18,9 +18,11 @@ package cmd import ( "bytes" + "io" "io/ioutil" "os" "path/filepath" + "strings" "testing" . "github.com/onsi/gomega" @@ -29,18 +31,22 @@ import ( func Test_generateYAML(t *testing.T) { g := NewWithT(t) // create a local template - template, cleanup1 := createTempFile(g, `v1: ${VAR1:=default1} + contents := `v1: ${VAR1:=default1} v2: ${VAR2=default2} -v3: ${VAR3:-default3}`) +v3: ${VAR3:-default3}` + template, cleanup1 := createTempFile(g, contents) defer cleanup1() templateWithoutVars, cleanup2 := createTempFile(g, `v1: foobar v2: bazfoo`) defer cleanup2() + inputReader := strings.NewReader(contents) + tests := []struct { name string options *generateYAMLOptions + inputReader io.Reader expectErr bool expectedOutput string }{ @@ -79,6 +85,16 @@ v3: default3 expectErr: false, expectedOutput: "\n", }, + { + name: "prints processed yaml using specified reader when '--from=-'", + options: &generateYAMLOptions{url: "-", listVariables: false}, + inputReader: inputReader, + expectErr: false, + expectedOutput: `v1: default1 +v2: default2 +v3: default3 +`, + }, } for _, tt := range tests { @@ -86,7 +102,7 @@ v3: default3 g := NewWithT(t) gyOpts = tt.options buf := bytes.NewBufferString("") - err := generateYAML(buf) + err := generateYAML(inputReader, buf) if tt.expectErr { g.Expect(err).To(HaveOccurred()) return diff --git a/docs/book/src/clusterctl/commands/generate-yaml.md b/docs/book/src/clusterctl/commands/generate-yaml.md index e4a7109d454f..853ebd74a548 100644 --- a/docs/book/src/clusterctl/commands/generate-yaml.md +++ b/docs/book/src/clusterctl/commands/generate-yaml.md @@ -22,6 +22,16 @@ clusterctl generate yaml --from https://github.com/foo-org/foo-repository/blob/m # Generates a configuration file with variable values using # a template stored locally. clusterctl generate yaml --from ~/workspace/cluster-template.yaml + +# Prints list of variables used in the local template +clusterctl generate yaml --from ~/workspace/cluster-template.yaml --list-variables + +# Prints list of variables from template passed in via stdin +cat ~/workspace/cluster-template.yaml | clusterctl generate yaml --from - --list-variables + +# Default behavior for this sub-command is to read from stdin. +# Generate configuration from stdin +cat ~/workspace/cluster-template.yaml | clusterctl generate yaml ```