diff --git a/cmd/languages.go b/cmd/languages.go index 4af02926..f55c3da7 100644 --- a/cmd/languages.go +++ b/cmd/languages.go @@ -27,13 +27,13 @@ DESCRIPTION This includes embedded (included) language runtimes as well as any installed via the 'repositories add' command. + To specify a URI of a single, specific repository for which languages + should be displayed, use the --repository flag. + Installed repositories are by default located at ~/.func/repositories ($XDG_CONFIG_HOME/.func/repositories). This can be overridden with $FUNC_REPOSITORIES_PATH. - To specify a URI of a single, specific repository for which languages - should be displayed, use the --repository flag. - To see templates available for a given language, see the 'templates' command. @@ -53,7 +53,7 @@ EXAMPLES PreRunE: bindEnv("json", "repository"), } - cmd.Flags().BoolP("json", "", false, "Set output to JSON format: $FUNC_JSON)") + cmd.Flags().BoolP("json", "", false, "Set output to JSON format. (Env: $FUNC_JSON)") cmd.Flags().StringP("repository", "r", "", "URI to a specific repository to consider (Env: $FUNC_REPOSITORY)") cmd.SetHelpFunc(defaultTemplatedHelp) diff --git a/cmd/root.go b/cmd/root.go index 9c4d9677..96513f20 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -99,6 +99,8 @@ EXAMPLES cmd.AddCommand(NewCompletionCmd()) cmd.AddCommand(NewVersionCmd(config.Version)) cmd.AddCommand(NewLanguagesCmd(newClient)) + cmd.AddCommand(NewTemplatesCmd(newClient)) + cmd.AddCommand(NewLanguagesCmd(newClient)) // Help // Overridden to process the help text as a template and have diff --git a/cmd/templates.go b/cmd/templates.go new file mode 100644 index 00000000..cca8ae49 --- /dev/null +++ b/cmd/templates.go @@ -0,0 +1,176 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/ory/viper" + "github.com/spf13/cobra" + + fn "knative.dev/kn-plugin-func" +) + +func NewTemplatesCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "templates", + Short: "Templates", + Long: ` +NAME + {{.Name}} templates - list available templates + +SYNOPSIS + {{.Name}} templates [language] [--json] [-r|--repository] + +DESCRIPTION + List all templates available, optionally for a specific language runtime. + + To specify a URI of a single, specific repository for which templates + should be displayed, use the --repository flag. + + Installed repositories are by default located at ~/.func/repositories + ($XDG_CONFIG_HOME/.func/repositories). This can be overridden with + $FUNC_REPOSITORIES_PATH. + + To see all available language runtimes, see the 'languages' command. + + +EXAMPLES + + o Show a list of all available templates grouped by language runtime + $ {{.Name}} templates + + o Show a list of all templates for the Go runtime + $ {{.Name}} templates go + + o Return a list of all template runtimes in JSON output format + $ {{.Name}} templates --json + + o Return Go templates in a specific repository + $ {{.Name}} templates go --repository=https://github.com/boson-project/func-templates +`, + SuggestFor: []string{"template", "templtaes", "templatse", "remplates", + "gemplates", "yemplates", "tenplates", "tekplates", "tejplates", + "temolates", "temllates", "temppates", "tempmates", "tempkates", + "templstes", "templztes", "templqtes", "templares", "templages", //nolint:misspell + "templayes", "templatee", "templatea", "templated", "templatew"}, + PreRunE: bindEnv("json", "repository"), + } + + cmd.Flags().BoolP("json", "", false, "Set output to JSON format. (Env: $FUNC_JSON)") + cmd.Flags().StringP("repository", "r", "", "URI to a specific repository to consider (Env: $FUNC_REPOSITORY)") + + cmd.SetHelpFunc(defaultTemplatedHelp) + + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return runTemplates(cmd, args, newClient) + } + + return cmd +} + +func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + // Gather config + cfg, err := newTemplatesConfig(newClient) + if err != nil { + return + } + + // Client which will provide data + client, done := newClient(ClientConfig{Verbose: cfg.Verbose}, + fn.WithRepository(cfg.Repository), // Use exactly this repo OR + fn.WithRepositoriesPath(cfg.RepositoriesPath)) // Path on disk to installed repos + defer done() + + // For a singl language runtime + // ------------------- + if len(args) == 1 { + templates, err := client.Templates().List(args[0]) + if err != nil { + return err + } + if cfg.JSON { + s, err := json.MarshalIndent(templates, "", " ") + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), string(s)) + } else { + for _, template := range templates { + fmt.Fprintln(cmd.OutOrStdout(), template) + } + } + return nil + } else if len(args) > 1 { + return errors.New("unexpected extra arguments.") + } + + // All language runtimes + // ------------ + runtimes, err := client.Runtimes() + if err != nil { + return + } + if cfg.JSON { + // Gather into a single data structure for printing as json + templateMap := make(map[string][]string) + for _, runtime := range runtimes { + templates, err := client.Templates().List(runtime) + if err != nil { + return err + } + templateMap[runtime] = templates + } + s, err := json.MarshalIndent(templateMap, "", " ") + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), string(s)) + } else { + // print using a formatted writer (sorted) + builder := strings.Builder{} + writer := tabwriter.NewWriter(&builder, 0, 0, 3, ' ', 0) + fmt.Fprint(writer, "LANGUAGE\tTEMPLATE\n") + for _, runtime := range runtimes { + templates, err := client.Templates().List(runtime) + if err != nil { + return err + } + for _, template := range templates { + fmt.Fprintf(writer, "%v\t%v\n", runtime, template) + } + } + writer.Flush() + fmt.Fprint(cmd.OutOrStdout(), builder.String()) + } + return +} + +type templatesConfig struct { + Verbose bool + Repository string // Consider only a specific repository (URI) + RepositoriesPath string // Override location on disk of "installed" repos + JSON bool // output as JSON +} + +func newTemplatesConfig(newClient ClientFactory) (cfg templatesConfig, err error) { + // Repositories Path + // Not exposed as a flag due to potential confusion with the more likely + // "repository" flag, but still available as an environment variable + repositoriesPath := os.Getenv("FUNC_REPOSITORIES_PATH") + if repositoriesPath == "" { // if no env var provided + repositoriesPath = fn.New().RepositoriesPath() // default to ~/.config/func/repositories + } + + cfg = templatesConfig{ + Verbose: viper.GetBool("verbose"), + Repository: viper.GetString("repository"), + RepositoriesPath: repositoriesPath, + JSON: viper.GetBool("json"), + } + + return +} diff --git a/cmd/templates_test.go b/cmd/templates_test.go new file mode 100644 index 00000000..3ff10fa1 --- /dev/null +++ b/cmd/templates_test.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "testing" + + fn "knative.dev/kn-plugin-func" + . "knative.dev/kn-plugin-func/testing" +) + +// TestTemplates_Default ensures that the default behavior is listing all +// templates for all language runtimes. +func TestTemplates_Default(t *testing.T) { + defer WithEnvVar(t, "XDG_CONFIG_HOME", t.TempDir())() // ignore user-added + buf := piped(t) // gather output + cmd := NewTemplatesCmd(NewClientFactory(func() *fn.Client { + return fn.New() + })) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + expected := `LANGUAGE TEMPLATE +go cloudevents +go http +node cloudevents +node http +python cloudevents +python http +quarkus cloudevents +quarkus http +rust cloudevents +rust http +springboot cloudevents +springboot http +typescript cloudevents +typescript http` + output := buf() + if output != expected { + t.Fatalf("expected:\n'%v'\n\ngot:\n'%v'\n", expected, output) + } +} + +// TestTemplates_JSON ensures that listing templates respects the --json +// output format. +func TestTemplates_JSON(t *testing.T) { + defer WithEnvVar(t, "XDG_CONFIG_HOME", t.TempDir())() // ignore user-added + buf := piped(t) // gather output + cmd := NewTemplatesCmd(NewClientFactory(func() *fn.Client { + return fn.New() + })) + cmd.SetArgs([]string{"--json"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + expected := `{ + "go": [ + "cloudevents", + "http" + ], + "node": [ + "cloudevents", + "http" + ], + "python": [ + "cloudevents", + "http" + ], + "quarkus": [ + "cloudevents", + "http" + ], + "rust": [ + "cloudevents", + "http" + ], + "springboot": [ + "cloudevents", + "http" + ], + "typescript": [ + "cloudevents", + "http" + ] +}` + + output := buf() + for i, c := range expected { + if len(output) <= i { + t.Fatalf("output missing character(s) '%v', '%s' and later\n", i, string(c)) + } + if rune(output[i]) != c { + t.Fatalf("Character at index %v expected '%s', got '%s'\n", i, string(c), string(output[i])) + } + } + + if output != expected { + t.Fatalf("expected:\n%v\ngot:\n%v\n", expected, output) + } +} + +// TestTemplates_ByLanguage ensures that the output is correctly filtered +// by language runtime when provided. +func TestTemplates_ByLanguage(t *testing.T) { + defer WithEnvVar(t, "XDG_CONFIG_HOME", t.TempDir())() // ignore user-added + cmd := NewTemplatesCmd(NewClientFactory(func() *fn.Client { + return fn.New() + })) + + // Test plain text + buf := piped(t) + cmd.SetArgs([]string{"go"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + expected := `cloudevents +http` + + output := buf() + if output != expected { + t.Fatalf("expected plain text:\n'%v'\ngot:\n'%v'\n", expected, output) + } + + // Test JSON output + buf = piped(t) + cmd.SetArgs([]string{"go", "--json"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + expected = `[ + "cloudevents", + "http" +]` + + output = buf() + if output != expected { + t.Fatalf("expected JSON:\n'%v'\ngot:\n'%v'\n", expected, output) + } + +}