From d81fa491349f03e24497473ca6d4f80e9f5e394d Mon Sep 17 00:00:00 2001 From: Luke Kingland Date: Tue, 14 Mar 2023 22:51:17 +0900 Subject: [PATCH 1/2] feat: metadata commands group --- README.md | 2 + cmd/config.go | 170 -------- cmd/config_git.go | 54 --- cmd/config_labels.go | 287 ------------- cmd/config_labels_test.go | 191 -------- cmd/config_test.go | 181 -------- cmd/config_volumes.go | 257 ----------- cmd/{config_envs.go => envs.go} | 281 +++++++----- cmd/envs_test.go | 163 +++++++ cmd/{config_git_set.go => git.go} | 48 ++- cmd/labels.go | 406 ++++++++++++++++++ cmd/labels_test.go | 263 ++++++++++++ cmd/metadata.go | 46 ++ cmd/root.go | 26 +- cmd/volumes.go | 391 +++++++++++++++++ cmd/volumes_test.go | 150 +++++++ docs/reference/func.md | 5 +- docs/reference/func_config.md | 32 -- docs/reference/func_config_envs.md | 31 -- docs/reference/func_config_labels.md | 30 -- docs/reference/func_config_labels_add.md | 31 -- docs/reference/func_config_volumes.md | 30 -- docs/reference/func_config_volumes_add.md | 28 -- docs/reference/func_deploy.md | 6 +- docs/reference/func_envs.md | 30 ++ ...nc_config_envs_add.md => func_envs_add.md} | 22 +- ...fig_envs_remove.md => func_envs_remove.md} | 6 +- docs/reference/func_git.md | 29 ++ docs/reference/func_git_set.md | 41 ++ docs/reference/func_info.md | 47 -- docs/reference/func_labels.md | 30 ++ docs/reference/func_labels_add.md | 43 ++ ...labels_remove.md => func_labels_remove.md} | 6 +- docs/reference/func_volumes.md | 31 ++ docs/reference/func_volumes_add.md | 34 ++ ...lumes_remove.md => func_volumes_remove.md} | 6 +- pkg/config/config.go | 11 +- pkg/config/config_test.go | 1 + 38 files changed, 1924 insertions(+), 1522 deletions(-) delete mode 100644 cmd/config.go delete mode 100644 cmd/config_git.go delete mode 100644 cmd/config_labels.go delete mode 100644 cmd/config_labels_test.go delete mode 100644 cmd/config_test.go delete mode 100644 cmd/config_volumes.go rename cmd/{config_envs.go => envs.go} (67%) create mode 100644 cmd/envs_test.go rename cmd/{config_git_set.go => git.go} (88%) create mode 100644 cmd/labels.go create mode 100644 cmd/labels_test.go create mode 100644 cmd/metadata.go create mode 100644 cmd/volumes.go create mode 100644 cmd/volumes_test.go delete mode 100644 docs/reference/func_config.md delete mode 100644 docs/reference/func_config_envs.md delete mode 100644 docs/reference/func_config_labels.md delete mode 100644 docs/reference/func_config_labels_add.md delete mode 100644 docs/reference/func_config_volumes.md delete mode 100644 docs/reference/func_config_volumes_add.md create mode 100644 docs/reference/func_envs.md rename docs/reference/{func_config_envs_add.md => func_envs_add.md} (60%) rename docs/reference/{func_config_envs_remove.md => func_envs_remove.md} (76%) create mode 100644 docs/reference/func_git.md create mode 100644 docs/reference/func_git_set.md delete mode 100644 docs/reference/func_info.md create mode 100644 docs/reference/func_labels.md create mode 100644 docs/reference/func_labels_add.md rename docs/reference/{func_config_labels_remove.md => func_labels_remove.md} (75%) create mode 100644 docs/reference/func_volumes.md create mode 100644 docs/reference/func_volumes_add.md rename docs/reference/{func_config_volumes_remove.md => func_volumes_remove.md} (75%) diff --git a/README.md b/README.md index 0108e5b87a..7432d3c4ad 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,5 @@ We use GitHub issues and project to track our roadmap. Please see our roadmap [h We are always looking for contributions from the Function Developer community. For more information on how to participate, see the [Contribuiting Guide](docs/CONTRIBUTING.md) The `func` Working Group meets @ 10:00 US Eastern every Tuesday, we'd love to have you! For more information, see the invitation on the [Knative Team Calendar](https://calendar.google.com/calendar/u/0/embed?src=knative.team_9q83bg07qs5b9rrslp5jor4l6s@group.calendar.google.com). + + diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index 28d53492b5..0000000000 --- a/cmd/config.go +++ /dev/null @@ -1,170 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/AlecAivazis/survey/v2" - "github.com/ory/viper" - "github.com/spf13/cobra" - - "knative.dev/func/pkg/config" - fn "knative.dev/func/pkg/functions" -) - -type functionLoader interface { - Load(path string) (fn.Function, error) -} - -type functionSaver interface { - Save(f fn.Function) error -} - -type functionLoaderSaver interface { - functionLoader - functionSaver -} - -type standardLoaderSaver struct{} - -func (s standardLoaderSaver) Load(path string) (fn.Function, error) { - f, err := fn.NewFunction(path) - if err != nil { - return fn.Function{}, fmt.Errorf("failed to create new function (path: %q): %w", path, err) - } - if !f.Initialized() { - return fn.Function{}, fmt.Errorf("the given path '%v' does not contain an initialized function", path) - } - return f, nil -} - -func (s standardLoaderSaver) Save(f fn.Function) error { - return f.Write() -} - -var defaultLoaderSaver standardLoaderSaver - -func NewConfigCmd(loadSaver functionLoaderSaver, newClient ClientFactory) *cobra.Command { - cmd := &cobra.Command{ - Use: "config", - Short: "Configure a function", - Long: `Configure a function - -Interactive prompt that allows configuration of Git configuration, Volume mounts, Environment -variables, and Labels for a function project present in the current directory -or from the directory specified with --path. -`, - SuggestFor: []string{"cfg", "cofnig"}, - PreRunE: bindEnv("path", "verbose"), - RunE: runConfigCmd, - } - cfg, err := config.NewDefault() - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) - } - - addPathFlag(cmd) - addVerboseFlag(cmd, cfg.Verbose) - - cmd.AddCommand(NewConfigGitCmd(newClient)) - cmd.AddCommand(NewConfigLabelsCmd(loadSaver)) - cmd.AddCommand(NewConfigEnvsCmd(loadSaver)) - cmd.AddCommand(NewConfigVolumesCmd()) - - return cmd -} - -func runConfigCmd(cmd *cobra.Command, args []string) (err error) { - - function, err := initConfigCommand(defaultLoaderSaver) - if err != nil { - return - } - - var qs = []*survey.Question{ - { - Name: "selectedConfig", - Prompt: &survey.Select{ - Message: "What do you want to configure?", - Options: []string{"Git", "Environment variables", "Volumes", "Labels"}, - Default: "Git", - }, - }, - { - Name: "selectedOperation", - Prompt: &survey.Select{ - Message: "What operation do you want to perform?", - Options: []string{"Add", "Remove", "List"}, - Default: "List", - }, - }, - } - - answers := struct { - SelectedConfig string - SelectedOperation string - }{} - - err = survey.Ask(qs, &answers) - if err != nil { - return - } - - switch answers.SelectedOperation { - case "Add": - if answers.SelectedConfig == "Volumes" { - err = runAddVolumesPrompt(cmd.Context(), function) - } else if answers.SelectedConfig == "Environment variables" { - err = runAddEnvsPrompt(cmd.Context(), function) - } else if answers.SelectedConfig == "Labels" { - err = runAddLabelsPrompt(cmd.Context(), function, defaultLoaderSaver) - } else if answers.SelectedConfig == "Git" { - err = runConfigGitSetCmd(cmd, NewClient) - } - case "Remove": - if answers.SelectedConfig == "Volumes" { - err = runRemoveVolumesPrompt(function) - } else if answers.SelectedConfig == "Environment variables" { - err = runRemoveEnvsPrompt(function) - } else if answers.SelectedConfig == "Labels" { - err = runRemoveLabelsPrompt(function, defaultLoaderSaver) - } - case "List": - if answers.SelectedConfig == "Volumes" { - listVolumes(function) - } else if answers.SelectedConfig == "Environment variables" { - err = listEnvs(function, cmd.OutOrStdout(), Human) - } else if answers.SelectedConfig == "Labels" { - listLabels(function) - } else if answers.SelectedConfig == "Git" { - err = runConfigGitCmd(cmd, NewClient) - } - } - - return -} - -// CLI Configuration (parameters) -// ------------------------------ - -type configCmdConfig struct { - Path string - Verbose bool -} - -func newConfigCmdConfig() configCmdConfig { - return configCmdConfig{ - Path: viper.GetString("path"), - Verbose: viper.GetBool("verbose"), - } -} - -func initConfigCommand(loader functionLoader) (fn.Function, error) { - config := newConfigCmdConfig() - - function, err := loader.Load(config.Path) - if err != nil { - return fn.Function{}, fmt.Errorf("failed to load the function (path: %q): %w", config.Path, err) - } - - return function, nil -} diff --git a/cmd/config_git.go b/cmd/config_git.go deleted file mode 100644 index 9b0bf6702a..0000000000 --- a/cmd/config_git.go +++ /dev/null @@ -1,54 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "knative.dev/func/pkg/config" - fn "knative.dev/func/pkg/functions" -) - -func NewConfigGitCmd(newClient ClientFactory) *cobra.Command { - cmd := &cobra.Command{ - Use: "git", - Short: "Manage Git configuration of a function", - Long: `Manage Git configuration of a function - -Prints Git configuration for a function project present in -the current directory or from the directory specified with --path. -`, - SuggestFor: []string{"gti", "Git", "Gti"}, - PreRunE: bindEnv("path"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - return runConfigGitCmd(cmd, newClient) - }, - } - // Global Config - cfg, err := config.NewDefault() - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) - } - - // Function Context - f, _ := fn.NewFunction(effectivePath()) - if f.Initialized() { - cfg = cfg.Apply(f) - } - - configGitSetCmd := NewConfigGitSetCmd(newClient) - - addPathFlag(cmd) - addVerboseFlag(cmd, cfg.Verbose) - - cmd.AddCommand(configGitSetCmd) - - return cmd -} - -func runConfigGitCmd(cmd *cobra.Command, newClient ClientFactory) (err error) { - fmt.Printf("--------------------------- Function Git config ---------------------------\n") - fmt.Printf("Not implemented yet.\n") - - return nil -} diff --git a/cmd/config_labels.go b/cmd/config_labels.go deleted file mode 100644 index e2474ffbc7..0000000000 --- a/cmd/config_labels.go +++ /dev/null @@ -1,287 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "os" - - "github.com/AlecAivazis/survey/v2" - "github.com/spf13/cobra" - - "knative.dev/func/pkg/config" - fn "knative.dev/func/pkg/functions" - "knative.dev/func/pkg/utils" -) - -func NewConfigLabelsCmd(loaderSaver functionLoaderSaver) *cobra.Command { - var configLabelsCmd = &cobra.Command{ - Use: "labels", - Short: "List and manage configured labels for a function", - Long: `List and manage configured labels for a function - -Prints configured labels for a function project present in -the current directory or from the directory specified with --path. -`, - Aliases: []string{"label"}, - SuggestFor: []string{"albels", "abels"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(loaderSaver) - if err != nil { - return - } - - listLabels(function) - - return - }, - } - - var configLabelsAddCmd = &cobra.Command{ - Use: "add", - Short: "Add labels to the function configuration", - Long: `Add labels to the function configuration - -Interactive prompt to add labels to the function project in the current -directory or from the directory specified with --path. - -The label can be set directly from a value or from an environment variable on -the local machine. -`, - SuggestFor: []string{"ad", "create", "insert", "append"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(loaderSaver) - if err != nil { - return - } - - return runAddLabelsPrompt(cmd.Context(), function, loaderSaver) - }, - } - - var configLabelsRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "Remove labels from the function configuration", - Long: `Remove labels from the function configuration - -Interactive prompt to remove labels from the function project in the current -directory or from the directory specified with --path. -`, - Aliases: []string{"rm"}, - SuggestFor: []string{"del", "delete", "rmeove"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(loaderSaver) - if err != nil { - return - } - - return runRemoveLabelsPrompt(function, loaderSaver) - }, - } - - cfg, err := config.NewDefault() - if err != nil { - fmt.Fprintf(configLabelsCmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) - } - - addPathFlag(configLabelsCmd) - addPathFlag(configLabelsAddCmd) - addPathFlag(configLabelsRemoveCmd) - addVerboseFlag(configLabelsCmd, cfg.Verbose) - addVerboseFlag(configLabelsAddCmd, cfg.Verbose) - addVerboseFlag(configLabelsRemoveCmd, cfg.Verbose) - - configLabelsCmd.AddCommand(configLabelsAddCmd) - configLabelsCmd.AddCommand(configLabelsRemoveCmd) - - return configLabelsCmd -} - -func listLabels(f fn.Function) { - if len(f.Deploy.Labels) == 0 { - fmt.Println("There aren't any configured labels") - return - } - - fmt.Println("Configured labels:") - for _, e := range f.Deploy.Labels { - fmt.Println(" - ", e.String()) - } -} - -func runAddLabelsPrompt(ctx context.Context, f fn.Function, saver functionSaver) (err error) { - - insertToIndex := 0 - - // SECTION - if there are some labels already set, choose the position of the new entry - if len(f.Deploy.Labels) > 0 { - options := []string{} - for _, e := range f.Deploy.Labels { - options = append(options, fmt.Sprintf("Insert before: %s", e.String())) - } - options = append(options, "Insert here.") - - selectedLabel := "" - prompt := &survey.Select{ - Message: "Where do you want to add the label?", - Options: options, - Default: options[len(options)-1], - } - err = survey.AskOne(prompt, &selectedLabel) - if err != nil { - return - } - - for i, option := range options { - if option == selectedLabel { - insertToIndex = i - break - } - } - } - - // SECTION - select the type of label to be added - selectedOption := "" - const ( - optionLabelValue = "Label with a specified value" - optionLabelLocal = "Value from a local environment variable" - ) - options := []string{optionLabelValue, optionLabelLocal} - - err = survey.AskOne(&survey.Select{ - Message: "What type of label do you want to add?", - Options: options, - }, &selectedOption) - if err != nil { - return - } - - newPair := fn.Label{} - - switch selectedOption { - // SECTION - add new label with the specified value - case optionLabelValue: - qs := []*survey.Question{ - { - Name: "key", - Prompt: &survey.Input{Message: "Please specify the label key:"}, - Validate: func(val interface{}) error { - return utils.ValidateLabelKey(val.(string)) - }, - }, - { - Name: "value", - Prompt: &survey.Input{Message: "Please specify the label value:"}, - Validate: func(val interface{}) error { - return utils.ValidateLabelValue(val.(string)) - }}, - } - answers := struct { - Key string - Value string - }{} - - err = survey.Ask(qs, &answers) - if err != nil { - return - } - - newPair.Key = &answers.Key - newPair.Value = &answers.Value - - // SECTION - add new label with value from a local environment variable - case optionLabelLocal: - qs := []*survey.Question{ - { - Name: "key", - Prompt: &survey.Input{Message: "Please specify the label key:"}, - Validate: func(val interface{}) error { - return utils.ValidateLabelKey(val.(string)) - }, - }, - { - Name: "value", - Prompt: &survey.Input{Message: "Please specify the local environment variable:"}, - Validate: func(val interface{}) error { - return utils.ValidateLabelValue(val.(string)) - }, - }, - } - answers := struct { - Key string - Value string - }{} - - err = survey.Ask(qs, &answers) - if err != nil { - return - } - - if _, ok := os.LookupEnv(answers.Value); !ok { - fmt.Printf("Warning: specified local environment variable %q is not set\n", answers.Value) - } - - value := fmt.Sprintf("{{ env:%s }}", answers.Value) - newPair.Key = &answers.Key - newPair.Value = &value - } - - // we have all necessary information -> let's insert the label to the selected position in the list - if insertToIndex == len(f.Deploy.Labels) { - f.Deploy.Labels = append(f.Deploy.Labels, newPair) - } else { - f.Deploy.Labels = append(f.Deploy.Labels[:insertToIndex+1], f.Deploy.Labels[insertToIndex:]...) - f.Deploy.Labels[insertToIndex] = newPair - } - - err = saver.Save(f) - if err == nil { - fmt.Println("Label entry was added to the function configuration") - } - - return -} - -func runRemoveLabelsPrompt(f fn.Function, saver functionSaver) (err error) { - if len(f.Deploy.Labels) == 0 { - fmt.Println("There aren't any configured labels") - return - } - - options := []string{} - for _, e := range f.Deploy.Labels { - options = append(options, e.String()) - } - - selectedLabel := "" - prompt := &survey.Select{ - Message: "Which labels do you want to remove?", - Options: options, - } - err = survey.AskOne(prompt, &selectedLabel) - if err != nil { - return - } - - var newLabels []fn.Label - removed := false - for i, e := range f.Deploy.Labels { - if e.String() == selectedLabel { - newLabels = append(f.Deploy.Labels[:i], f.Deploy.Labels[i+1:]...) - removed = true - break - } - } - - if removed { - f.Deploy.Labels = newLabels - err = saver.Save(f) - if err == nil { - fmt.Println("Label was removed from the function configuration") - } - } - - return -} diff --git a/cmd/config_labels_test.go b/cmd/config_labels_test.go deleted file mode 100644 index dbe5222ae2..0000000000 --- a/cmd/config_labels_test.go +++ /dev/null @@ -1,191 +0,0 @@ -//go:build linux -// +build linux - -package cmd - -import ( - "context" - "os" - "reflect" - "sync" - "testing" - "time" - - "github.com/Netflix/go-expect" - "github.com/hinshun/vt10x" - "github.com/spf13/cobra" - fn "knative.dev/func/pkg/functions" -) - -type mockFunctionLoaderSaver struct { - f fn.Function -} - -func (m *mockFunctionLoaderSaver) Load(path string) (fn.Function, error) { - return m.f, nil -} - -func (m *mockFunctionLoaderSaver) Save(f fn.Function) error { - m.f = f - return nil -} - -func assertLabelEq(t *testing.T, actual []fn.Label, want []fn.Label) { - t.Helper() - if !reflect.DeepEqual(actual, want) { - t.Errorf("labels = %v, want %v", actual, want) - } -} - -func createRunFunc(cmd *cobra.Command, t *testing.T) func(subcmd string, input ...string) { - return func(subcmd string, input ...string) { - - ctx := context.Background() - c, _, err := vt10x.NewVT10XConsole() - if err != nil { - t.Fatal(err) - } - defer c.Close() - - var wg sync.WaitGroup - wg.Add(1) - go func() { - //defer wg.Done() - _, _ = c.ExpectEOF() - }() - go func() { - defer wg.Done() - time.Sleep(time.Millisecond * 50) - for _, s := range input { - _, _ = c.Send(s) - time.Sleep(time.Millisecond * 50) - } - }() - - a := []string{subcmd} - cmd.SetArgs(a) - - func() { - defer withMockedStdio(t, c)() - err = cmd.ExecuteContext(ctx) - wg.Wait() - }() - if err != nil { - t.Fatal(err) - } - } -} - -func withMockedStdio(t *testing.T, c *expect.Console) func() { - t.Helper() - - oldIn := os.Stdin - oldOut := os.Stdout - oldErr := os.Stderr - - os.Stdin = c.Tty() - os.Stdout = c.Tty() - os.Stderr = c.Tty() - - return func() { - os.Stdin = oldIn - os.Stdout = oldOut - os.Stderr = oldErr - } -} - -const ( - arrowUp = "\033[A" - arrowDown = "\033[B" - enter = "\r" -) - -func TestNewConfigLabelsCmd(t *testing.T) { - - var loaderSaver mockFunctionLoaderSaver - labels := &loaderSaver.f.Deploy.Labels - - cmd := NewConfigLabelsCmd(&loaderSaver) - cmd.SetArgs([]string{}) - - run := createRunFunc(cmd, t) - - p := func(k, v string) fn.Label { - return fn.Label{Key: &k, Value: &v} - } - - assertLabel := func(ps []fn.Label) { - t.Helper() - assertLabelEq(t, *labels, ps) - } - - run("add", enter, "a", enter, "b", enter) - assertLabel([]fn.Label{p("a", "b")}) - - run("add", enter, enter, "c", enter, "d", enter) - assertLabel([]fn.Label{p("a", "b"), p("c", "d")}) - - run("add", arrowUp, arrowUp, enter, enter, "e", enter, "f", enter) - assertLabel([]fn.Label{p("e", "f"), p("a", "b"), p("c", "d")}) - - run("remove", arrowDown, enter) - assertLabel([]fn.Label{p("e", "f"), p("c", "d")}) -} - -func TestListLabels(t *testing.T) { - - p := func(k, v string) fn.Label { - return fn.Label{Key: &k, Value: &v} - } - - var loaderSaver mockFunctionLoaderSaver - labels := &loaderSaver.f.Deploy.Labels - - *labels = append(*labels, p("a", "b"), p("c", "d")) - - cmd := NewConfigLabelsCmd(&loaderSaver) - cmd.SetArgs([]string{}) - - ctx := context.Background() - c, _, err := vt10x.NewVT10XConsole() - if err != nil { - t.Fatal(err) - } - defer c.Close() - - errChan := make(chan error, 1) - func() { - var err error - defer func() { - errChan <- err - }() - defer withMockedStdio(t, c)() - err = cmd.ExecuteContext(ctx) - }() - - expected := []string{ - `Configured labels:`, - `- Label with key "a" and value "b"`, - `- Label with key "c" and value "d"`, - } - - // prevents the ExpectString() function from waiting indefinitely - // in case when expected string is not printed to stdout nor the stdout is closed - go func() { - time.Sleep(time.Second * 5) - c.Close() - }() - - for _, s := range expected { - out, err := c.ExpectString(s) - if err != nil { - t.Errorf("unexpected output: %q, err: %v\n", out, err) - } - } - - err = <-errChan - if err != nil { - t.Fatal(err) - } - -} diff --git a/cmd/config_test.go b/cmd/config_test.go deleted file mode 100644 index 31bd49f7c2..0000000000 --- a/cmd/config_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package cmd_test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "sort" - "testing" - - "github.com/ory/viper" - fnCmd "knative.dev/func/cmd" - fn "knative.dev/func/pkg/functions" -) - -func TestListEnvs(t *testing.T) { - mock := newMockLoaderSaver() - foo := "foo" - bar := "bar" - envs := []fn.Env{{Name: &foo, Value: &bar}} - mock.load = func(path string) (fn.Function, error) { - if path != "" { - t.Fatalf("bad path, got %q but expected ", path) - } - return fn.Function{Run: fn.RunSpec{Envs: envs}}, nil - } - - cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "-o=json", "--path="}) - - var buff bytes.Buffer - cmd.SetOut(&buff) - cmd.SetErr(&buff) - - err := cmd.Execute() - if err != nil { - t.Fatal(err) - } - - var data []fn.Env - err = json.Unmarshal(buff.Bytes(), &data) - if err != nil { - t.Fatal(err) - } - if !envsEqual(envs, data) { - t.Errorf("env mismatch, expedted %v but got %v", envs, data) - } -} - -func TestListEnvAdd(t *testing.T) { - // strings as vars so we can take address of them - foo := "foo" - bar := "bar" - answer := "answer" - fortyTwo := "42" - configMapExpression := "{{ configMap:myMap }}" - - mock := newMockLoaderSaver() - mock.load = func(path string) (fn.Function, error) { - return fn.Function{Run: fn.RunSpec{Envs: []fn.Env{{Name: &foo, Value: &bar}}}}, nil - } - var expectedEnvs []fn.Env - mock.save = func(f fn.Function) error { - if !envsEqual(expectedEnvs, f.Run.Envs) { - return fmt.Errorf("unexpected envs: got %v but %v was expected", f.Run.Envs, expectedEnvs) - } - return nil - } - - expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: &answer, Value: &fortyTwo}} - cmd := fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--name=answer", "--value=42"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - err := cmd.Execute() - if err != nil { - t.Error(err) - } - - viper.Reset() - expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: nil, Value: &configMapExpression}} - cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--value={{ configMap:myMap }}"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - err = cmd.Execute() - if err != nil { - t.Error(err) - } - - viper.Reset() - cmd = fnCmd.NewConfigCmd(mock, fnCmd.NewClient) - cmd.SetArgs([]string{"envs", "add", "--name=1", "--value=abc"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - err = cmd.Execute() - if err == nil { - t.Error("expected variable name error but got nil") - } -} - -func envsEqual(a, b []fn.Env) bool { - if len(a) != len(b) { - return false - } - - strPtrEq := func(x, y *string) bool { - switch { - case x == nil && y == nil: - return true - case x != nil && y != nil: - return *x == *y - default: - return false - } - } - - strPtrLess := func(x, y *string) bool { - switch { - case x == nil && y == nil: - return false - case x != nil && y != nil: - return *x < *y - case x == nil: - return true - default: - return false - } - - } - - lessForSlice := func(s []fn.Env) func(i, j int) bool { - return func(i, j int) bool { - x := s[i] - y := s[j] - if strPtrLess(x.Name, y.Name) { - return true - } - return strPtrLess(x.Value, y.Value) - } - } - - sort.Slice(a, lessForSlice(a)) - sort.Slice(b, lessForSlice(b)) - - for i := range a { - x := a[i] - y := b[i] - if !strPtrEq(x.Name, y.Name) || !strPtrEq(x.Value, y.Value) { - return false - } - } - return true -} - -func newMockLoaderSaver() *mockLoaderSaver { - return &mockLoaderSaver{ - load: func(path string) (fn.Function, error) { - return fn.Function{}, nil - }, - save: func(f fn.Function) error { - return nil - }, - } -} - -type mockLoaderSaver struct { - load func(path string) (fn.Function, error) - save func(f fn.Function) error -} - -func (m mockLoaderSaver) Load(path string) (fn.Function, error) { - return m.load(path) -} - -func (m mockLoaderSaver) Save(f fn.Function) error { - return m.save(f) -} diff --git a/cmd/config_volumes.go b/cmd/config_volumes.go deleted file mode 100644 index 7cb1874852..0000000000 --- a/cmd/config_volumes.go +++ /dev/null @@ -1,257 +0,0 @@ -package cmd - -import ( - "context" - "fmt" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/spf13/cobra" - - "knative.dev/func/pkg/config" - fn "knative.dev/func/pkg/functions" - "knative.dev/func/pkg/k8s" -) - -func NewConfigVolumesCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "volumes", - Short: "List and manage configured volumes for a function", - Long: `List and manage configured volumes for a function - -Prints configured Volume mounts for a function project present in -the current directory or from the directory specified with --path. -`, - Aliases: []string{"volume"}, - SuggestFor: []string{"vol", "volums", "vols"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(defaultLoaderSaver) - if err != nil { - return - } - - listVolumes(function) - - return - }, - } - cfg, err := config.NewDefault() - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) - } - - configVolumesAddCmd := NewConfigVolumesAddCmd() - configVolumesRemoveCmd := NewConfigVolumesRemoveCmd() - - addPathFlag(cmd) - addPathFlag(configVolumesAddCmd) - addPathFlag(configVolumesRemoveCmd) - - addVerboseFlag(cmd, cfg.Verbose) - addVerboseFlag(configVolumesAddCmd, cfg.Verbose) - addVerboseFlag(configVolumesRemoveCmd, cfg.Verbose) - - cmd.AddCommand(configVolumesAddCmd) - cmd.AddCommand(configVolumesRemoveCmd) - - return cmd -} - -func NewConfigVolumesAddCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "add", - Short: "Add volume to the function configuration", - Long: `Add volume to the function configuration - -Interactive prompt to add Secrets and ConfigMaps as Volume mounts to the function project -in the current directory or from the directory specified with --path. -`, - SuggestFor: []string{"ad", "create", "insert", "append"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(defaultLoaderSaver) - if err != nil { - return - } - - return runAddVolumesPrompt(cmd.Context(), function) - }, - } - - return cmd -} - -func NewConfigVolumesRemoveCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "remove", - Short: "Remove volume from the function configuration", - Long: `Remove volume from the function configuration - -Interactive prompt to remove Volume mounts from the function project -in the current directory or from the directory specified with --path. -`, - Aliases: []string{"rm"}, - SuggestFor: []string{"del", "delete", "rmeove"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(defaultLoaderSaver) - if err != nil { - return - } - - return runRemoveVolumesPrompt(function) - }, - } - - return cmd -} - -func listVolumes(f fn.Function) { - if len(f.Run.Volumes) == 0 { - fmt.Println("There aren't any configured Volume mounts") - return - } - - fmt.Println("Configured Volumes mounts:") - for _, v := range f.Run.Volumes { - fmt.Println(" - ", v.String()) - } -} - -func runAddVolumesPrompt(ctx context.Context, f fn.Function) (err error) { - - secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace) - if err != nil { - return - } - configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace) - if err != nil { - return - } - - // SECTION - select resource type to be mounted - options := []string{} - selectedOption := "" - const optionConfigMap = "ConfigMap" - const optionSecret = "Secret" - - if len(configMaps) > 0 { - options = append(options, optionConfigMap) - } - if len(secrets) > 0 { - options = append(options, optionSecret) - } - - switch len(options) { - case 0: - fmt.Printf("There aren't any Secrets or ConfiMaps in the namespace \"%s\"\n", f.Deploy.Namespace) - return - case 1: - selectedOption = options[0] - case 2: - err = survey.AskOne(&survey.Select{ - Message: "What do you want to mount as a Volume?", - Options: options, - }, &selectedOption) - if err != nil { - return - } - } - - // SECTION - select the specific resource to be mounted - optionsResoures := []string{} - resourceType := "" - switch selectedOption { - case optionConfigMap: - resourceType = optionConfigMap - optionsResoures = configMaps - case optionSecret: - resourceType = optionSecret - optionsResoures = secrets - } - - selectedResource := "" - err = survey.AskOne(&survey.Select{ - Message: fmt.Sprintf("Which \"%s\" do you want to mount?", resourceType), - Options: optionsResoures, - }, &selectedResource) - if err != nil { - return - } - - // SECTION - specify mount Path of the Volume - - path := "" - err = survey.AskOne(&survey.Input{ - Message: fmt.Sprintf("Please specify the path where the %s should be mounted:", resourceType), - }, &path, survey.WithValidator(func(val interface{}) error { - if str, ok := val.(string); !ok || len(str) <= 0 || !strings.HasPrefix(str, "/") { - return fmt.Errorf("The input must be non-empty absolute path.") - } - return nil - })) - if err != nil { - return - } - - // we have all necessary information -> let's store the new Volume - newVolume := fn.Volume{Path: &path} - switch selectedOption { - case optionConfigMap: - newVolume.ConfigMap = &selectedResource - case optionSecret: - newVolume.Secret = &selectedResource - } - - f.Run.Volumes = append(f.Run.Volumes, newVolume) - - err = f.Write() - if err == nil { - fmt.Println("Volume entry was added to the function configuration") - } - - return -} - -func runRemoveVolumesPrompt(f fn.Function) (err error) { - if len(f.Run.Volumes) == 0 { - fmt.Println("There aren't any configured Volume mounts") - return - } - - options := []string{} - for _, v := range f.Run.Volumes { - options = append(options, v.String()) - } - - selectedVolume := "" - prompt := &survey.Select{ - Message: "Which Volume do you want to remove?", - Options: options, - } - err = survey.AskOne(prompt, &selectedVolume) - if err != nil { - return - } - - var newVolumes []fn.Volume - removed := false - for i, v := range f.Run.Volumes { - if v.String() == selectedVolume { - newVolumes = append(f.Run.Volumes[:i], f.Run.Volumes[i+1:]...) - removed = true - break - } - } - - if removed { - f.Run.Volumes = newVolumes - err = f.Write() - if err == nil { - fmt.Println("Volume entry was removed from the function configuration") - } - } - - return -} diff --git a/cmd/config_envs.go b/cmd/envs.go similarity index 67% rename from cmd/config_envs.go rename to cmd/envs.go index 6f4e7690fe..ffbdcc74c1 100644 --- a/cmd/config_envs.go +++ b/cmd/envs.go @@ -1,16 +1,13 @@ package cmd import ( - "context" "encoding/json" "errors" "fmt" - "io" "os" "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" - "github.com/ory/viper" "github.com/spf13/cobra" "knative.dev/func/pkg/config" @@ -19,56 +16,85 @@ import ( "knative.dev/func/pkg/utils" ) -func NewConfigEnvsCmd(loadSaver functionLoaderSaver) *cobra.Command { +func NewEnvsCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "envs", - Short: "List and manage configured environment variable for a function", - Long: `List and manage configured environment variable for a function + Short: "Manage function environment variables", + Long: `{{rootCmdUse}} envs -Prints configured Environment variable for a function project present in -the current directory or from the directory specified with --path. -`, +Manages function environment variables. Default is to list currently configured +environment variables for the function. See subcommands 'add' and 'remove'.`, Aliases: []string{"env"}, SuggestFor: []string{"ensv"}, PreRunE: bindEnv("path", "output", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(loadSaver) - if err != nil { - return - } - - return listEnvs(function, cmd.OutOrStdout(), Format(viper.GetString("output"))) + RunE: func(cmd *cobra.Command, _ []string) error { + return runEnvs(cmd, newClient) }, } + + // Global Config cfg, err := config.NewDefault() if err != nil { fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) } - cmd.Flags().StringP("output", "o", "human", "Output format (human|json) (Env: $FUNC_OUTPUT)") - - configEnvsAddCmd := NewConfigEnvsAddCmd(loadSaver) - configEnvsRemoveCmd := NewConfigEnvsRemoveCmd() - + // Flags + addVerboseFlag(cmd, cfg.Verbose) addPathFlag(cmd) - addPathFlag(configEnvsAddCmd) - addPathFlag(configEnvsRemoveCmd) - addVerboseFlag(cmd, cfg.Verbose) - addVerboseFlag(configEnvsAddCmd, cfg.Verbose) - addVerboseFlag(configEnvsRemoveCmd, cfg.Verbose) + // TODO: use global config Output setting, treating empty string as deafult + // to mean human-optimized. + cmd.Flags().StringP("output", "o", "human", "Output format (human|json) (Env: $FUNC_OUTPUT)") - cmd.AddCommand(configEnvsAddCmd) - cmd.AddCommand(configEnvsRemoveCmd) + // Subcommands + cmd.AddCommand(NewEnvsAddCmd(newClient)) + cmd.AddCommand(NewEnvsRemoveCmd(newClient)) return cmd } -func NewConfigEnvsAddCmd(loadSaver functionLoaderSaver) *cobra.Command { +func runEnvs(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // update f with metadata cfg + return + } + + switch Format(cfg.Output) { + case Human: + if len(f.Run.Envs) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No environment variables") + return + } + fmt.Fprintln(cmd.OutOrStdout(), "Environment variables:") + for _, v := range f.Run.Envs { + fmt.Fprintf(cmd.OutOrStdout(), " %s\n", v) + } + return + case JSON: + return json.NewEncoder(cmd.OutOrStdout()).Encode(f.Run.Envs) + default: + fmt.Fprintf(cmd.ErrOrStderr(), "invalid format: %v", cfg.Output) + return + } +} + +func NewEnvsAddCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "add", - Short: "Add environment variable to the function configuration", - Long: `Add environment variable to the function configuration. + Short: "Add environment variable to the function", + Long: `Add environment variable to the function. If environment variable is not set explicitly by flag, interactive prompt is used. @@ -76,118 +102,95 @@ The environment variable can be set directly from a value, from an environment variable on the local machine or from Secrets and ConfigMaps. It is also possible to import all keys as environment variables from a Secret or ConfigMap.`, Example: `# set environment variable directly -{{rootCmdUse}} config envs add --name=VARNAME --value=myValue +{{rootCmdUse}} envs add --name=VARNAME --value=myValue # set environment variable from local env $LOC_ENV -{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} env:LOC_ENV {{"}}"}}' +{{rootCmdUse}} envs add --name=VARNAME --value='{{"{{"}} env:LOC_ENV {{"}}"}}' set environment variable from a secret -{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} secret:secretName:key {{"}}"}}' +{{rootCmdUse}} envs add --name=VARNAME --value='{{"{{"}} secret:secretName:key {{"}}"}}' # set all key as environment variables from a secret -{{rootCmdUse}} config envs add --value='{{"{{"}} secret:secretName {{"}}"}}' +{{rootCmdUse}} envs add --value='{{"{{"}} secret:secretName {{"}}"}}' # set environment variable from a configMap -{{rootCmdUse}} config envs add --name=VARNAME --value='{{"{{"}} configMap:confMapName:key {{"}}"}}' +{{rootCmdUse}} envs add --name=VARNAME --value='{{"{{"}} configMap:confMapName:key {{"}}"}}' # set all key as environment variables from a configMap -{{rootCmdUse}} config envs add --value='{{"{{"}} configMap:confMapName {{"}}"}}'`, +{{rootCmdUse}} envs add --value='{{"{{"}} configMap:confMapName {{"}}"}}'`, SuggestFor: []string{"ad", "create", "insert", "append"}, - PreRunE: bindEnv("path", "name", "value", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(loadSaver) - if err != nil { - return - } - - var np *string - var vp *string - - if cmd.Flags().Changed("name") { - s, e := cmd.Flags().GetString("name") - if e != nil { - return e - } - np = &s - } - if cmd.Flags().Changed("value") { - s, e := cmd.Flags().GetString("value") - if e != nil { - return e - } - vp = &s - } - - if np != nil || vp != nil { - if np != nil { - if err := utils.ValidateEnvVarName(*np); err != nil { - return err - } - } - - function.Run.Envs = append(function.Run.Envs, fn.Env{Name: np, Value: vp}) - return loadSaver.Save(function) - } - - return runAddEnvsPrompt(cmd.Context(), function) + PreRunE: bindEnv("verbose", "path", "name", "value"), + RunE: func(cmd *cobra.Command, _ []string) error { + return runEnvsAdd(cmd, newClient) }, } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addVerboseFlag(cmd, cfg.Verbose) + addPathFlag(cmd) cmd.Flags().StringP("name", "", "", "Name of the environment variable.") cmd.Flags().StringP("value", "", "", "Value of the environment variable.") return cmd } -func NewConfigEnvsRemoveCmd() *cobra.Command { - return &cobra.Command{ - Use: "remove", - Short: "Remove environment variable from the function configuration", - Long: `Remove environment variable from the function configuration - -Interactive prompt to remove Environment variables from the function project -in the current directory or from the directory specified with --path. -`, - Aliases: []string{"rm"}, - SuggestFor: []string{"del", "delete", "rmeove"}, - PreRunE: bindEnv("path", "verbose"), - RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(defaultLoaderSaver) - if err != nil { - return - } - - return runRemoveEnvsPrompt(function) - }, +func runEnvsAdd(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // update f with metadata cfg + return } -} - -func listEnvs(f fn.Function, w io.Writer, outputFormat Format) error { - switch outputFormat { - case Human: - if len(f.Run.Envs) == 0 { - _, err := fmt.Fprintln(w, "There aren't any configured Environment variables") - return err + // TODO: the below implementation was imported verbatim from config_envs.go + // which was written before global and local config was created. + // As such it needs to be refactored to use name and value flags from config, + // the validation function of config, and have its major sections extracted. + // Furthermore, the core functionality should probably be in the core + // client library and merely invoked from this CLI such that users of the + // client library have access to these features. + + var np *string + var vp *string + + if cmd.Flags().Changed("name") { + s, e := cmd.Flags().GetString("name") + if e != nil { + return e + } + np = &s + } + if cmd.Flags().Changed("value") { + s, e := cmd.Flags().GetString("value") + if e != nil { + return e } + vp = &s + } - fmt.Println("Configured Environment variables:") - for _, e := range f.Run.Envs { - _, err := fmt.Fprintln(w, " - ", e.String()) - if err != nil { + if np != nil || vp != nil { + if np != nil { + if err := utils.ValidateEnvVarName(*np); err != nil { return err } } - return nil - case JSON: - enc := json.NewEncoder(w) - return enc.Encode(f.Run.Envs) - default: - return fmt.Errorf("bad format: %v", outputFormat) - } -} -func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) { + f.Run.Envs = append(f.Run.Envs, fn.Env{Name: np, Value: vp}) + return f.Write() + } insertToIndex := 0 @@ -219,11 +222,11 @@ func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) { } // SECTION - select the type of Environment variable to be added - secrets, err := k8s.ListSecretsNamesIfConnected(ctx, f.Deploy.Namespace) + secrets, err := k8s.ListSecretsNamesIfConnected(cmd.Context(), f.Deploy.Namespace) if err != nil { return } - configMaps, err := k8s.ListConfigMapsNamesIfConnected(ctx, f.Deploy.Namespace) + configMaps, err := k8s.ListConfigMapsNamesIfConnected(cmd.Context(), f.Deploy.Namespace) if err != nil { return } @@ -464,7 +467,49 @@ func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) { return } -func runRemoveEnvsPrompt(f fn.Function) (err error) { +func NewEnvsRemoveCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "Remove environment variable from the function configuration", + Long: `Remove environment variable from the function configuration + +Interactive prompt to remove Environment variables from the function project +in the current directory or from the directory specified with --path. +`, + Aliases: []string{"rm"}, + SuggestFor: []string{"del", "delete", "rmeove"}, + PreRunE: bindEnv("path", "verbose"), + RunE: func(cmd *cobra.Command, _ []string) error { + return runEnvsRemove(cmd, newClient) + }, + } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addVerboseFlag(cmd, cfg.Verbose) + addPathFlag(cmd) + return cmd +} + +func runEnvsRemove(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // update f with metadata cfg + return + } + if len(f.Run.Envs) == 0 { fmt.Println("There aren't any configured Environment variables") return diff --git a/cmd/envs_test.go b/cmd/envs_test.go new file mode 100644 index 0000000000..6080b94cb5 --- /dev/null +++ b/cmd/envs_test.go @@ -0,0 +1,163 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" + "testing" + + fn "knative.dev/func/pkg/functions" +) + +// TestEnvs_DefaultEmpty ensures that the default is a list which responds correctly +// when no environment variables specified on the function, in both default and +// json output. +func TestEnvs_DefaultEmpty(t *testing.T) { + root := fromTempDirectory(t) + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Empty list, default output (human legible) + cmd := NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "No environment variables" { + t.Fatalf("Unexpected result from an empty envs list:\n%v\n", out) + } + + // Empty list, json output + cmd = NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Env + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, []fn.Env{}) { + t.Fatalf("unexpected data from an empty function: %s\n", bb) + } +} + +// TestEnvs_DefaultPopulated ensures that environment variables on a function are +// listed in both default and json formats. +func TestEnvs_DefaultPopulated(t *testing.T) { + root := fromTempDirectory(t) + + name := "name" // TODO: pointers unnecessary + value := "value" + envs := []fn.Env{{Name: &name, Value: &value}} + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Run: fn.RunSpec{Envs: envs}}); err != nil { + t.Fatal(err) + } + + // Populated list, default formatting + cmd := NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "Environment variables:\n Env \"name\" with value \"value\"" { + t.Fatalf("Unexpected envs list:\n%v\n", out) + } + + // Populated list, json output + cmd = NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Env + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, envs) { + t.Fatalf("unexpected output: %s\n", bb) + } +} + +// TestEnvs_Add ensures that adding an environment variable in all available +// ways succeeds or fails as expected: +// - simple key/value succeeds +// - as a configMap value succeeds +// - with an invalid key fails +func TestEnvs_Add(t *testing.T) { + root := fromTempDirectory(t) + + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Add a simple key/value + cmd := NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--name=name", "--value=value"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + name := "name" + value := "value" + envs := []fn.Env{{Name: &name, Value: &value}} // TODO: pointers unnecessary + if !reflect.DeepEqual(f.Run.Envs, envs) { + t.Fatalf("unexpected envs: %v\n", f.Run.Envs) + } + + // Add a configMap value reference should not fail + cmd = NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--value={{ configMap:myMap }}"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err = fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + name = "name" + value = "value" + configMapValue := "{{ configMap:myMap }}" + envs = []fn.Env{{Name: &name, Value: &value}, {Name: nil, Value: &configMapValue}} // TODO: pointers unnecessary + if !reflect.DeepEqual(f.Run.Envs, envs) { + t.Fatalf("unexpected envs: %v\n", f.Run.Envs) + } + + // Add with an invalid (numeric first character) name should fail + cmd = NewEnvsCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--name=1", "--value=value"}) + if err := cmd.Execute(); err == nil { + t.Fatal("did not receive expected error adding env with invalid name") + } +} + +// TestEnvs_Remove ensures that removing environment variables succeeds +// TODO: Not Implemented +// Thee is currently no way other than interactive prompts to remove +// a declared environment variable. Note this is a bit tricky because some +// have no key (no name) indicating "all variables from the given reference". +// These entries could be removed instead by specifying an exact match of. +// their value string, or their via their index in the list. + +// TODO: TestEnvs_Interactive diff --git a/cmd/config_git_set.go b/cmd/git.go similarity index 88% rename from cmd/config_git_set.go rename to cmd/git.go index 7f62fcb46d..3937b4ea04 100644 --- a/cmd/config_git_set.go +++ b/cmd/git.go @@ -6,12 +6,58 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/ory/viper" "github.com/spf13/cobra" + "knative.dev/func/pkg/config" fn "knative.dev/func/pkg/functions" "knative.dev/func/pkg/pipelines" ) -func NewConfigGitSetCmd(newClient ClientFactory) *cobra.Command { +func NewGitCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "git", + Short: "Manage Git configuration of a function", + Long: `Manage Git configuration of a function + +Prints Git configuration for a function project present in +the current directory or from the directory specified with --path. +`, + SuggestFor: []string{"gti", "Git", "Gti"}, + PreRunE: bindEnv("path"), + RunE: func(cmd *cobra.Command, args []string) (err error) { + return runConfigGitCmd(cmd, newClient) + }, + } + + // Global Config + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + + // Function Context + f, _ := fn.NewFunction(effectivePath()) + if f.Initialized() { + cfg = cfg.Apply(f) + } + + // Flags + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + // Subcommands + cmd.AddCommand(NewGitSetCmd(newClient)) + + return cmd +} + +func runConfigGitCmd(cmd *cobra.Command, newClient ClientFactory) (err error) { + fmt.Printf("--------------------------- Function Git config ---------------------------\n") + fmt.Printf("Not implemented yet.\n") + + return nil +} + +func NewGitSetCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "set", Short: "Set Git settings in the function configuration", diff --git a/cmd/labels.go b/cmd/labels.go new file mode 100644 index 0000000000..36740cf286 --- /dev/null +++ b/cmd/labels.go @@ -0,0 +1,406 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + + "knative.dev/func/pkg/config" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/utils" +) + +func NewLabelsCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "labels", + Short: "Manage function labels", + Long: `{{rootCmdUse}} labels + +Manages function labels. Default is to list currently configured labels. See +subcommands 'add' and 'remove'.`, + Aliases: []string{"label"}, + SuggestFor: []string{"albels", "abels"}, + PreRunE: bindEnv("path", "output", "verbose"), + RunE: func(cmd *cobra.Command, _ []string) (err error) { + return runLabels(cmd, newClient) + }, + } + + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + + // Flags + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + // TODO: use global config Output setting, treating empty string as deafult + // to mean human-optimized. + cmd.Flags().StringP("output", "o", "human", "Output format (human|json) (Env: $FUNC_OUTPUT)") + + // Subcommands + cmd.AddCommand(NewLabelsAddCmd(newClient)) + cmd.AddCommand(NewLabelsRemoveCmd(newClient)) + + return cmd +} + +func runLabels(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { + return + } + + switch Format(cfg.Output) { + case Human: + if len(f.Deploy.Labels) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No labels") + return + } + fmt.Fprintln(cmd.OutOrStdout(), "Labels:") + for _, l := range f.Deploy.Labels { + fmt.Fprintf(cmd.OutOrStdout(), " %v\n", l) + } + return + case JSON: + return json.NewEncoder(cmd.OutOrStdout()).Encode(f.Deploy.Labels) + default: + fmt.Fprintf(cmd.ErrOrStderr(), "invalid format: %v", cfg.Output) + return + } +} + +func NewLabelsAddCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "Add label to the function", + Long: `Add labels to the function + +Add labels to the function project in the current directory or from the +directory specified with --path. If no flags are provided, addition is +completed using interactive prompts. + +The label can be set directly from a value or from an environment variable on +the local machine.`, + Example: `# add a label +{{rootCmdUse}} labels add --key=myLabel --value=myValue + +# add a label from a local environment variable +{{rootCmdUse}} labels add --key=myLabel --value='{{"{{"}} env:LOC_ENV {{"}}"}}'`, + SuggestFor: []string{"ad", "create", "insert", "append"}, + PreRunE: bindEnv("verbose", "path"), + RunE: func(cmd *cobra.Command, _ []string) error { + return runLabelsAdd(cmd, newClient) + }, + } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + cmd.Flags().StringP("key", "", "", "Key of the label.") + cmd.Flags().StringP("value", "", "", "Value of the label.") + + return cmd +} + +func runLabelsAdd(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { + return + } + + // TODO: The below implementation was was written prior to global config, + // local configs and the general command structure now used. + // As such, it needs to be refactored to use name and value flags from the + // command's config struct, to use the validation function, and to have its + // major sections extracted. + // Furthermore, the core functionality here should probably be in the core + // client library and merely invoked from this CLI such that users of the + // client library also have accss to these features. + + // Noninteractive + // -------------- + + var np *string + var vp *string + + if cmd.Flags().Changed("key") { + s, e := cmd.Flags().GetString("key") + if e != nil { + return e + } + np = &s + } + if cmd.Flags().Changed("value") { + s, e := cmd.Flags().GetString("value") + if e != nil { + return e + } + vp = &s + } + + // Validators + if np != nil { + if err := utils.ValidateLabelKey(*np); err != nil { + return err + } + } + if vp != nil { + if err := utils.ValidateLabelValue(*vp); err != nil { + return err + } + } + + // Set + if np != nil || vp != nil { + f.Deploy.Labels = append(f.Deploy.Labels, fn.Label{Key: np, Value: vp}) + return f.Write() + } + + // Interactive + // -------------- + + insertToIndex := 0 + + // SECTION - if there are some labels already set, choose the position of the new entry + if len(f.Deploy.Labels) > 0 { + options := []string{} + for _, e := range f.Deploy.Labels { + options = append(options, fmt.Sprintf("Insert before: %s", e.String())) + } + options = append(options, "Insert here.") + + selectedLabel := "" + prompt := &survey.Select{ + Message: "Where do you want to add the label?", + Options: options, + Default: options[len(options)-1], + } + err = survey.AskOne(prompt, &selectedLabel) + if err != nil { + return + } + + for i, option := range options { + if option == selectedLabel { + insertToIndex = i + break + } + } + } + + // SECTION - select the type of label to be added + selectedOption := "" + const ( + optionLabelValue = "Label with a specified value" + optionLabelLocal = "Value from a local environment variable" + ) + options := []string{optionLabelValue, optionLabelLocal} + + err = survey.AskOne(&survey.Select{ + Message: "What type of label do you want to add?", + Options: options, + }, &selectedOption) + if err != nil { + return + } + + newPair := fn.Label{} + + switch selectedOption { + // SECTION - add new label with the specified value + case optionLabelValue: + qs := []*survey.Question{ + { + Name: "key", + Prompt: &survey.Input{Message: "Please specify the label key:"}, + Validate: func(val interface{}) error { + return utils.ValidateLabelKey(val.(string)) + }, + }, + { + Name: "value", + Prompt: &survey.Input{Message: "Please specify the label value:"}, + Validate: func(val interface{}) error { + return utils.ValidateLabelValue(val.(string)) + }}, + } + answers := struct { + Key string + Value string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + return + } + + newPair.Key = &answers.Key + newPair.Value = &answers.Value + + // SECTION - add new label with value from a local environment variable + case optionLabelLocal: + qs := []*survey.Question{ + { + Name: "key", + Prompt: &survey.Input{Message: "Please specify the label key:"}, + Validate: func(val interface{}) error { + return utils.ValidateLabelKey(val.(string)) + }, + }, + { + Name: "value", + Prompt: &survey.Input{Message: "Please specify the local environment variable:"}, + Validate: func(val interface{}) error { + return utils.ValidateLabelValue(val.(string)) + }, + }, + } + answers := struct { + Key string + Value string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + return + } + + if _, ok := os.LookupEnv(answers.Value); !ok { + fmt.Printf("Warning: specified local environment variable %q is not set\n", answers.Value) + } + + value := fmt.Sprintf("{{ env:%s }}", answers.Value) + newPair.Key = &answers.Key + newPair.Value = &value + } + + // we have all necessary information -> let's insert the label to the selected position in the list + if insertToIndex == len(f.Deploy.Labels) { + f.Deploy.Labels = append(f.Deploy.Labels, newPair) + } else { + f.Deploy.Labels = append(f.Deploy.Labels[:insertToIndex+1], f.Deploy.Labels[insertToIndex:]...) + f.Deploy.Labels[insertToIndex] = newPair + } + + err = f.Write() + if err == nil { + fmt.Println("Label entry was added to the function configuration") + } + + return +} + +func NewLabelsRemoveCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "Remove labels from the function configuration", + Long: `Remove labels from the function configuration + +Interactive prompt to remove labels from the function project in the current +directory or from the directory specified with --path. +`, + Aliases: []string{"rm"}, + SuggestFor: []string{"del", "delete", "rmeove"}, + PreRunE: bindEnv("verbose", "path"), + RunE: func(cmd *cobra.Command, _ []string) error { + return runLabelsRemove(cmd, newClient) + }, + } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + return cmd +} + +func runLabelsRemove(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // update f with cfg if applicable + return + } + + if len(f.Deploy.Labels) == 0 { + fmt.Println("There aren't any configured labels") + return + } + + options := []string{} + for _, e := range f.Deploy.Labels { + options = append(options, e.String()) + } + + selectedLabel := "" + prompt := &survey.Select{ + Message: "Which labels do you want to remove?", + Options: options, + } + err = survey.AskOne(prompt, &selectedLabel) + if err != nil { + return + } + + var newLabels []fn.Label + removed := false + for i, e := range f.Deploy.Labels { + if e.String() == selectedLabel { + newLabels = append(f.Deploy.Labels[:i], f.Deploy.Labels[i+1:]...) + removed = true + break + } + } + + if removed { + f.Deploy.Labels = newLabels + err = f.Write() + if err == nil { + fmt.Println("Label was removed from the function configuration") + } + } + return +} diff --git a/cmd/labels_test.go b/cmd/labels_test.go new file mode 100644 index 0000000000..b0ccd5d8f7 --- /dev/null +++ b/cmd/labels_test.go @@ -0,0 +1,263 @@ +package cmd + +import ( + "bytes" + "context" + "encoding/json" + "os" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/Netflix/go-expect" + "github.com/hinshun/vt10x" + "github.com/spf13/cobra" + fn "knative.dev/func/pkg/functions" +) + +// TestLabels_DefaultEmpty ensures that the default is a list which responds +// correctly when no labels are specified, in both default and json output +func TestLabels_DefaultEmpty(t *testing.T) { + root := fromTempDirectory(t) + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Empty list, default output (human legible) + cmd := NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "No labels" { + t.Fatalf("Unexpected result from an empty labels list:\n%v\n", out) + } + + // Empty list, json output + cmd = NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Label + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, []fn.Label{}) { + t.Fatalf("unexpected data from an empty function: %s\n", bb) + } +} + +// TestLabels_DefaultPopulated ensures that labels on a function are +// listed in both default and json formats. +func TestLabels_DefaultPopulated(t *testing.T) { + root := fromTempDirectory(t) + + key := "key" + value := "value" + labels := []fn.Label{{Key: &key, Value: &value}} // TODO: pointers unnecessary + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Deploy: fn.DeploySpec{Labels: labels}}); err != nil { + t.Fatal(err) + } + + // Populated list, default formatting + cmd := NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "Labels:\n Label with key \"key\" and value \"value\"" { + t.Fatalf("Unexpected labels list:\n%v\n", out) + } + + // Populated list, json output + cmd = NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Label + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, labels) { + t.Fatalf("expected output: %s got: %s\n", labels, data) + } +} + +// TestLabels_Add ensures adding labels works as expected. +func TestLabels_Add(t *testing.T) { + root := fromTempDirectory(t) + + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Add a simple value + cmd := NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--key=key", "--value=value"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + key := "key" // TODO: pointers unnecessary + value := "value" + labels := []fn.Label{{Key: &key, Value: &value}} + if !reflect.DeepEqual(f.Deploy.Labels, labels) { + t.Fatalf("unexpected labels: %v\n", f.Deploy.Labels) + } + + // Add an environment variable + cmd = NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--key=key", "--value={{ env:MYENV }}"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err = fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + key2 := "key" + value2 := "{{ env:MYENV }}" + labels = []fn.Label{ + {Key: &key, Value: &value}, + {Key: &key2, Value: &value2}, + } + if !reflect.DeepEqual(f.Deploy.Labels, labels) { + t.Fatalf("expected labels: \n%v\nGot labels: \n%v\n", labels, f.Deploy.Labels) + } + + // TODO: check errors when label or key invalid +} + +// TestLabels_Remove ensures that removing works as expected +func TestLabels_Remove(t *testing.T) { +} + +// TestLabels_Interactive ensures the expected interactive flow succeeds. +func TestLabels_Interactive(t *testing.T) { + root := fromTempDirectory(t) + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + cmd := NewLabelsCmd(NewTestClient()) + cmd.SetArgs([]string{}) + + run := createRunFunc(cmd, t) + + p := func(k, v string) fn.Label { // TODO: pointers unnecessary + return fn.Label{Key: &k, Value: &v} + } + + assert := func(ll []fn.Label) { + assertLabels(t, root, ll) + } + + run("add", enter, "a", enter, "b", enter) + assert([]fn.Label{p("a", "b")}) + + run("add", enter, enter, "c", enter, "d", enter) + assert([]fn.Label{p("a", "b"), p("c", "d")}) + + run("add", arrowUp, arrowUp, enter, enter, "e", enter, "f", enter) + assert([]fn.Label{p("e", "f"), p("a", "b"), p("c", "d")}) + + run("remove", arrowDown, enter) + assert([]fn.Label{p("e", "f"), p("c", "d")}) +} + +func assertLabels(t *testing.T, root string, expected []fn.Label) { + t.Helper() + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(f.Deploy.Labels, expected) { + t.Errorf("expected labels: %v, got: %v", expected, f.Deploy.Labels) + } +} + +func createRunFunc(cmd *cobra.Command, t *testing.T) func(subcmd string, input ...string) { + return func(subcmd string, input ...string) { + + ctx := context.Background() + c, _, err := vt10x.NewVT10XConsole() + if err != nil { + t.Fatal(err) + } + defer c.Close() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + //defer wg.Done() + _, _ = c.ExpectEOF() + }() + go func() { + defer wg.Done() + time.Sleep(time.Millisecond * 50) + for _, s := range input { + _, _ = c.Send(s) + time.Sleep(time.Millisecond * 50) + } + }() + + a := []string{subcmd} + cmd.SetArgs(a) + + func() { + defer withMockedStdio(t, c)() + err = cmd.ExecuteContext(ctx) + wg.Wait() + }() + if err != nil { + t.Fatal(err) + } + } +} + +func withMockedStdio(t *testing.T, c *expect.Console) func() { + t.Helper() + + oldIn := os.Stdin + oldOut := os.Stdout + oldErr := os.Stderr + + os.Stdin = c.Tty() + os.Stdout = c.Tty() + os.Stderr = c.Tty() + + return func() { + os.Stdin = oldIn + os.Stdout = oldOut + os.Stderr = oldErr + } +} + +const ( + arrowUp = "\033[A" + arrowDown = "\033[B" + enter = "\r" +) diff --git a/cmd/metadata.go b/cmd/metadata.go new file mode 100644 index 0000000000..49b96dd6ee --- /dev/null +++ b/cmd/metadata.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "github.com/ory/viper" + "knative.dev/func/pkg/config" + fn "knative.dev/func/pkg/functions" +) + +// TODO: once functional break into a config for each of the metadata commands +// labels, envs, volumes + +type metadataConfig struct { + config.Global // includes verbose + Path string +} + +func newMetadataConfig() metadataConfig { + return metadataConfig{ + Global: config.Global{ + Verbose: viper.GetBool("verbose"), + Output: viper.GetString("output"), + }, + Path: viper.GetString("path"), + } +} + +func (c metadataConfig) Validate() error { + // TODO: validate cascate upwards like the constructor. + // c.Global.Validat() // should validate .Output + return nil +} + +func (c metadataConfig) Configure(f fn.Function) (fn.Function, error) { + f = c.Global.Configure(f) + return f, nil +} + +func (c metadataConfig) Prompt() (metadataConfig, error) { + if !interactiveTerminal() { + return c, nil + } + // + // TODO: move all the prompts here + // + return c, nil +} diff --git a/cmd/root.go b/cmd/root.go index e4a27455ab..32033c8ece 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -75,6 +75,15 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), NewListCmd(newClient), }, }, + { + Header: "Function Metadata:", + Commands: []*cobra.Command{ + NewGitCmd(newClient), + NewEnvsCmd(newClient), + NewLabelsCmd(newClient), + NewVolumesCmd(newClient), + }, + }, { Header: "Development Commands:", Commands: []*cobra.Command{ @@ -84,16 +93,16 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), }, }, { - Header: "System Commands:", + Header: "System Settings:", Commands: []*cobra.Command{ - NewConfigCmd(defaultLoaderSaver, newClient), + // NewConfigCmd(defaultLoaderSaver), // NOTE: Global Config being added in a separate PR NewLanguagesCmd(newClient), NewTemplatesCmd(newClient), NewRepositoryCmd(newClient), }, }, { - Header: "Other Commands:", + Header: "Other:", Commands: []*cobra.Command{ NewCompletionCmd(), NewVersionCmd(cfg.Version), @@ -132,10 +141,13 @@ func effectivePath() (path string) { ) fs.SetOutput(io.Discard) fs.ParseErrorsWhitelist.UnknownFlags = true // wokeignore:rule=whitelist - err := fs.Parse(os.Args[1:]) - if err != nil { - fmt.Fprintf(os.Stderr, "error preparsing flags: %v\n", err) - } + _ = fs.Parse(os.Args[1:]) + // NOTE: Errors ignored + // Preparsing is opportunistic, errors suppressed + // For example, if --help were passed this would error, despite + // the command which is instantiated and parses flags later understanding, + // and implementing the requst. This pre-parsing step simply looks + // for the path flag and is tolerante of any errors. if env != "" { path = env } diff --git a/cmd/volumes.go b/cmd/volumes.go new file mode 100644 index 0000000000..e664f96974 --- /dev/null +++ b/cmd/volumes.go @@ -0,0 +1,391 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + + "knative.dev/func/pkg/config" + fn "knative.dev/func/pkg/functions" + "knative.dev/func/pkg/k8s" +) + +func NewVolumesCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "volumes", + Short: "Manage function volumes", + Long: `{{rootCmdUse}} volumes + +Manges function volumes mounts. Default is to list currently configured +volumes. See subcommands 'add' and 'remove'. +`, + Aliases: []string{"volume", "vol", "vols"}, + SuggestFor: []string{"volums"}, + PreRunE: bindEnv("path", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runVolumes(cmd, newClient) + }, + } + + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + + // Flags + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + // TODO: use glboal config Output setting, treating empty string as default + // to mean human-optimized. + cmd.Flags().StringP("output", "o", "human", "Output format (human|json) (Env: $FUNC_OUTPUT)") + + // Subcommands + cmd.AddCommand(NewVolumesAddCmd(newClient)) + cmd.AddCommand(NewVolumesRemoveCmd(newClient)) + + return cmd +} + +func runVolumes(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { + return + } + + switch Format(cfg.Output) { + case Human: + if len(f.Run.Volumes) == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "No volumes") + return + } + fmt.Fprintln(cmd.OutOrStdout(), "Volumes:") + for _, v := range f.Run.Volumes { + fmt.Fprintf(cmd.OutOrStdout(), " %s\n", v) + } + return + case JSON: + return json.NewEncoder(cmd.OutOrStdout()).Encode(f.Run.Volumes) + default: + fmt.Fprintf(cmd.ErrOrStderr(), "invalid format: %v", cfg.Output) + return + } +} + +func NewVolumesAddCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "Add volume to the function", + Long: `Add volume to the function + +Add secrets and config maps as volume mounts to the function in the current +directory or from the directory specified by --path. If no flags are provided, +addition is completed using interactive prompts. + +The volume can be set +`, + SuggestFor: []string{"ad", "create", "insert", "append"}, + PreRunE: bindEnv("verbose", "path", "configmap", "secret", "mount"), + RunE: func(cmd *cobra.Command, args []string) error { + return runVolumesAdd(cmd, newClient) + }, + } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + cmd.Flags().StringP("configmap", "", "", "Name of the config map to mount.") + cmd.Flags().StringP("secret", "", "", "Name of the secret to mount.") + cmd.Flags().StringP("mount", "", "", "Mount path.") + + return cmd +} + +func runVolumesAdd(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { + return + } + + secrets, err := k8s.ListSecretsNamesIfConnected(cmd.Context(), f.Deploy.Namespace) + if err != nil { + return + } + configMaps, err := k8s.ListConfigMapsNamesIfConnected(cmd.Context(), f.Deploy.Namespace) + if err != nil { + return + } + + // TODO: the below implementation was created prior to global and local + // configuration, or the general command structure used in other commands. + // It is also a monolithic function. + // As such it is in need of some refactoring: + + // Noninteractive: + // ------------ + + var sp *string + var cp *string + var vp *string + + if cmd.Flags().Changed("secret") { + s, e := cmd.Flags().GetString("secret") + if e != nil { + return e + } + sp = &s + } + if cmd.Flags().Changed("configmap") { + s, e := cmd.Flags().GetString("configmap") + if e != nil { + return e + } + cp = &s + } + if cmd.Flags().Changed("mount") { + s, e := cmd.Flags().GetString("mount") + if e != nil { + return e + } + vp = &s + } + + // Validators + if cp != nil && sp != nil { + return errors.New("Only one of Confg Map OR secret may be specified when adding a volume.") + } + if (cp != nil || sp != nil) && vp == nil { + return errors.New("Path is required when either config map or secret name provided") + } + func() { // Warn if secret specified but not found + if sp == nil { + return + } + for _, v := range secrets { + if v == *sp { + return + } + } + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: the secret name '%v' was not found in the remote\n", *sp) + }() + func() { // Warn if config map specified but not found + if cp == nil { + return + } + for _, v := range configMaps { + if v == *cp { + return + } + } + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: the config map name '%v' was not found in the remote\n", *cp) + }() + + // Add and rturn + if cp != nil { + f.Run.Volumes = append(f.Run.Volumes, fn.Volume{ConfigMap: cp, Path: vp}) + return f.Write() + } else if sp != nil { + f.Run.Volumes = append(f.Run.Volumes, fn.Volume{Secret: sp, Path: vp}) + return f.Write() + } + + // Interactive: + // ------------ + + // SECTION - select resource type to be mounted + options := []string{} + selectedOption := "" + const optionConfigMap = "ConfigMap" + const optionSecret = "Secret" + + if len(configMaps) > 0 { + options = append(options, optionConfigMap) + } + if len(secrets) > 0 { + options = append(options, optionSecret) + } + + switch len(options) { + case 0: + fmt.Printf("There aren't any Secrets or ConfiMaps in the namespace \"%s\"\n", f.Deploy.Namespace) + return + case 1: + selectedOption = options[0] + case 2: + err = survey.AskOne(&survey.Select{ + Message: "What do you want to mount as a Volume?", + Options: options, + }, &selectedOption) + if err != nil { + return + } + } + + // SECTION - select the specific resource to be mounted + optionsResoures := []string{} + resourceType := "" + switch selectedOption { + case optionConfigMap: + resourceType = optionConfigMap + optionsResoures = configMaps + case optionSecret: + resourceType = optionSecret + optionsResoures = secrets + } + + selectedResource := "" + err = survey.AskOne(&survey.Select{ + Message: fmt.Sprintf("Which \"%s\" do you want to mount?", resourceType), + Options: optionsResoures, + }, &selectedResource) + if err != nil { + return + } + + // SECTION - specify mount Path of the Volume + + path := "" + err = survey.AskOne(&survey.Input{ + Message: fmt.Sprintf("Please specify the path where the %s should be mounted:", resourceType), + }, &path, survey.WithValidator(func(val interface{}) error { + if str, ok := val.(string); !ok || len(str) <= 0 || !strings.HasPrefix(str, "/") { + return fmt.Errorf("The input must be non-empty absolute path.") + } + return nil + })) + if err != nil { + return + } + + // we have all necessary information -> let's store the new Volume + newVolume := fn.Volume{Path: &path} + switch selectedOption { + case optionConfigMap: + newVolume.ConfigMap = &selectedResource + case optionSecret: + newVolume.Secret = &selectedResource + } + + f.Run.Volumes = append(f.Run.Volumes, newVolume) + + err = f.Write() + if err == nil { + fmt.Println("Volume entry was added to the function configuration") + } + + return +} + +func NewVolumesRemoveCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "Remove volume from the function configuration", + Long: `Remove volume from the function configuration + +Interactive prompt to remove Volume mounts from the function project +in the current directory or from the directory specified with --path. +`, + Aliases: []string{"rm"}, + SuggestFor: []string{"del", "delete", "rmeove"}, + PreRunE: bindEnv("verbose", "path"), + RunE: func(cmd *cobra.Command, args []string) error { + return runVolumesRemove(cmd, newClient) + }, + } + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + addPathFlag(cmd) + addVerboseFlag(cmd, cfg.Verbose) + + return cmd +} + +func runVolumesRemove(cmd *cobra.Command, newClient ClientFactory) (err error) { + var ( + cfg metadataConfig + f fn.Function + ) + if cfg, err = newMetadataConfig().Prompt(); err != nil { + return + } + if err = cfg.Validate(); err != nil { + return + } + if f, err = fn.NewFunction(cfg.Path); err != nil { + return + } + if f, err = cfg.Configure(f); err != nil { // update f with cfg if applicable + return + } + if len(f.Run.Volumes) == 0 { + fmt.Println("There aren't any configured Volume mounts") + return + } + + options := []string{} + for _, v := range f.Run.Volumes { + options = append(options, v.String()) + } + + selectedVolume := "" + prompt := &survey.Select{ + Message: "Which Volume do you want to remove?", + Options: options, + } + err = survey.AskOne(prompt, &selectedVolume) + if err != nil { + return + } + + var newVolumes []fn.Volume + removed := false + for i, v := range f.Run.Volumes { + if v.String() == selectedVolume { + newVolumes = append(f.Run.Volumes[:i], f.Run.Volumes[i+1:]...) + removed = true + break + } + } + + if removed { + f.Run.Volumes = newVolumes + err = f.Write() + if err == nil { + fmt.Println("Volume entry was removed from the function configuration") + } + } + + return +} diff --git a/cmd/volumes_test.go b/cmd/volumes_test.go new file mode 100644 index 0000000000..367c95ea0e --- /dev/null +++ b/cmd/volumes_test.go @@ -0,0 +1,150 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "reflect" + "strings" + "testing" + + fn "knative.dev/func/pkg/functions" +) + +// TestVolumes_DefaultEmpty ensures that the default is a list which responds +// correctly when no volumes are specified, in both default and json output +func TestVolumes_DefaultEmpty(t *testing.T) { + root := fromTempDirectory(t) + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Empty list, default output (human legible) + cmd := NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "No volumes" { + t.Fatalf("Unexpected result from an empty volumes list:\n%v\n", out) + } + + // Empty list, json output + cmd = NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Env + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, []fn.Env{}) { + t.Fatalf("unexpected data from an empty function: %s\n", bb) + } +} + +// TestVolumes_DefaultPopulated ensures that volumes on a function are +// listed in both default and json formats. +func TestVolumes_DefaultPopulated(t *testing.T) { + root := fromTempDirectory(t) + + secret := "secret" + path := "path" + volumes := []fn.Volume{{Secret: &secret, Path: &path}} // TODO: pointers unnecessary + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root, Run: fn.RunSpec{Volumes: volumes}}); err != nil { + t.Fatal(err) + } + + // Populated list, default formatting + cmd := NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{}) + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + out := strings.TrimSpace(buff.String()) + if out != "Volumes:\n Secret \"secret\" mounted at path: \"path\"" { + t.Fatalf("Unexpected volumes list:\n%v\n", out) + } + + // Populated list, json output + cmd = NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{"-o=json"}) + buff = bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + var data []fn.Volume + bb := buff.Bytes() + if err := json.Unmarshal(bb, &data); err != nil { + t.Fatal(err) // fail if not proper json + } + if !reflect.DeepEqual(data, volumes) { + t.Fatalf("expected output: %s got: %s\n", volumes, data) + } +} + +// TestVolumes_Add ensures adding volumes works as expected. +func TestVolumes_Add(t *testing.T) { + root := fromTempDirectory(t) + + if err := fn.New().Init(fn.Function{Runtime: "go", Root: root}); err != nil { + t.Fatal(err) + } + + // Add a configMap + cmd := NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--configmap=cm", "--mount=/cm"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err := fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + key := "cm" // TODO: pointers unnecessary + mount := "/cm" + volumes := []fn.Volume{{ConfigMap: &key, Path: &mount}} + if !reflect.DeepEqual(f.Run.Volumes, volumes) { + t.Fatalf("unexpected volumes: %v\n", f.Run.Volumes) + } + + // Add a secret + cmd = NewVolumesCmd(NewTestClient()) + cmd.SetArgs([]string{"add", "--secret=secret", "--mount=/secret"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + f, err = fn.NewFunction(root) + if err != nil { + t.Fatal(err) + } + key2 := "secret" + mount2 := "/secret" + volumes = []fn.Volume{ + {ConfigMap: &key, Path: &mount}, + {Secret: &key2, Path: &mount2}, + } + if !reflect.DeepEqual(f.Run.Volumes, volumes) { + t.Fatalf("unexpected volumes: %v\n", f.Run.Volumes) + } + + // TODO: check errors: both --secret and --configmap specified, or + // invalid value formats +} + +// TODO: TestVolumes_Remove + +// TODO: TestVolumes_Interactive diff --git a/docs/reference/func.md b/docs/reference/func.md index 1df7423b9b..9291c0e253 100644 --- a/docs/reference/func.md +++ b/docs/reference/func.md @@ -25,16 +25,19 @@ Learn more about Knative at: https://knative.dev * [func build](func_build.md) - Build a function container * [func completion](func_completion.md) - Output functions shell completion code -* [func config](func_config.md) - Configure a function * [func create](func_create.md) - Create a function * [func delete](func_delete.md) - Undeploy a function * [func deploy](func_deploy.md) - Deploy a function * [func describe](func_describe.md) - Describe a function +* [func envs](func_envs.md) - Manage function environment variables +* [func git](func_git.md) - Manage Git configuration of a function * [func invoke](func_invoke.md) - Invoke a local or remote function +* [func labels](func_labels.md) - Manage function labels * [func languages](func_languages.md) - List available function language runtimes * [func list](func_list.md) - List deployed functions * [func repository](func_repository.md) - Manage installed template repositories * [func run](func_run.md) - Run the function locally * [func templates](func_templates.md) - List available function source templates * [func version](func_version.md) - Function client version information +* [func volumes](func_volumes.md) - Manage function volumes diff --git a/docs/reference/func_config.md b/docs/reference/func_config.md deleted file mode 100644 index d46755de5e..0000000000 --- a/docs/reference/func_config.md +++ /dev/null @@ -1,32 +0,0 @@ -## func config - -Configure a function - -### Synopsis - -Configure a function - -Interactive prompt that allows configuration of Volume mounts, Environment -variables, and Labels for a function project present in the current directory -or from the directory specified with --path. - - -``` -func config -``` - -### Options - -``` - -h, --help help for config - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func](func.md) - func manages Knative Functions -* [func config envs](func_config_envs.md) - List and manage configured environment variable for a function -* [func config labels](func_config_labels.md) - List and manage configured labels for a function -* [func config volumes](func_config_volumes.md) - List and manage configured volumes for a function - diff --git a/docs/reference/func_config_envs.md b/docs/reference/func_config_envs.md deleted file mode 100644 index 738f8a37e0..0000000000 --- a/docs/reference/func_config_envs.md +++ /dev/null @@ -1,31 +0,0 @@ -## func config envs - -List and manage configured environment variable for a function - -### Synopsis - -List and manage configured environment variable for a function - -Prints configured Environment variable for a function project present in -the current directory or from the directory specified with --path. - - -``` -func config envs -``` - -### Options - -``` - -h, --help help for envs - -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func config](func_config.md) - Configure a function -* [func config envs add](func_config_envs_add.md) - Add environment variable to the function configuration -* [func config envs remove](func_config_envs_remove.md) - Remove environment variable from the function configuration - diff --git a/docs/reference/func_config_labels.md b/docs/reference/func_config_labels.md deleted file mode 100644 index 531a412016..0000000000 --- a/docs/reference/func_config_labels.md +++ /dev/null @@ -1,30 +0,0 @@ -## func config labels - -List and manage configured labels for a function - -### Synopsis - -List and manage configured labels for a function - -Prints configured labels for a function project present in -the current directory or from the directory specified with --path. - - -``` -func config labels -``` - -### Options - -``` - -h, --help help for labels - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func config](func_config.md) - Configure a function -* [func config labels add](func_config_labels_add.md) - Add labels to the function configuration -* [func config labels remove](func_config_labels_remove.md) - Remove labels from the function configuration - diff --git a/docs/reference/func_config_labels_add.md b/docs/reference/func_config_labels_add.md deleted file mode 100644 index 9a027ad3c6..0000000000 --- a/docs/reference/func_config_labels_add.md +++ /dev/null @@ -1,31 +0,0 @@ -## func config labels add - -Add labels to the function configuration - -### Synopsis - -Add labels to the function configuration - -Interactive prompt to add labels to the function project in the current -directory or from the directory specified with --path. - -The label can be set directly from a value or from an environment variable on -the local machine. - - -``` -func config labels add -``` - -### Options - -``` - -h, --help help for add - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func config labels](func_config_labels.md) - List and manage configured labels for a function - diff --git a/docs/reference/func_config_volumes.md b/docs/reference/func_config_volumes.md deleted file mode 100644 index 331c707a0b..0000000000 --- a/docs/reference/func_config_volumes.md +++ /dev/null @@ -1,30 +0,0 @@ -## func config volumes - -List and manage configured volumes for a function - -### Synopsis - -List and manage configured volumes for a function - -Prints configured Volume mounts for a function project present in -the current directory or from the directory specified with --path. - - -``` -func config volumes -``` - -### Options - -``` - -h, --help help for volumes - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func config](func_config.md) - Configure a function -* [func config volumes add](func_config_volumes_add.md) - Add volume to the function configuration -* [func config volumes remove](func_config_volumes_remove.md) - Remove volume from the function configuration - diff --git a/docs/reference/func_config_volumes_add.md b/docs/reference/func_config_volumes_add.md deleted file mode 100644 index b44ca89fbe..0000000000 --- a/docs/reference/func_config_volumes_add.md +++ /dev/null @@ -1,28 +0,0 @@ -## func config volumes add - -Add volume to the function configuration - -### Synopsis - -Add volume to the function configuration - -Interactive prompt to add Secrets and ConfigMaps as Volume mounts to the function project -in the current directory or from the directory specified with --path. - - -``` -func config volumes add -``` - -### Options - -``` - -h, --help help for add - -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func config volumes](func_config_volumes.md) - List and manage configured volumes for a function - diff --git a/docs/reference/func_deploy.md b/docs/reference/func_deploy.md index a1775b5d2d..a9aa15e8a3 100644 --- a/docs/reference/func_deploy.md +++ b/docs/reference/func_deploy.md @@ -108,9 +108,9 @@ func deploy --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) -e, --env stringArray Environment variable to set in the form NAME=VALUE. You may provide this flag multiple times for setting multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-). - -t, --git-branch string Git revision (branch) to be used when deploying via a git repository (Env: $FUNC_GIT_BRANCH) - -d, --git-dir string Directory in the repo to find the function (default is the root) (Env: $FUNC_GIT_DIR) - -g, --git-url string Repo url to push the code to be built (Env: $FUNC_GIT_URL) + -t, --git-branch string Git revision (branch) to be used when deploying via the Git repository (Env: $FUNC_GIT_BRANCH) + -d, --git-dir string Directory in the Git repository containing the function (default is the root) (Env: $FUNC_GIT_DIR)) + -g, --git-url string Repository url containing the function to build (Env: $FUNC_GIT_URL) -h, --help help for deploy -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. (Env: $FUNC_IMAGE) -n, --namespace string Deploy into a specific namespace. Will use function's current namespace by default if already deployed, and the currently active namespace if it can be determined. (Env: $FUNC_NAMESPACE) diff --git a/docs/reference/func_envs.md b/docs/reference/func_envs.md new file mode 100644 index 0000000000..316e6b65a7 --- /dev/null +++ b/docs/reference/func_envs.md @@ -0,0 +1,30 @@ +## func envs + +Manage function environment variables + +### Synopsis + +func envs + +Manages function environment variables. Default is to list currently configured +environment variables for the function. See subcommands 'add' and 'remove'. + +``` +func envs +``` + +### Options + +``` + -h, --help help for envs + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - func manages Knative Functions +* [func envs add](func_envs_add.md) - Add environment variable to the function +* [func envs remove](func_envs_remove.md) - Remove environment variable from the function configuration + diff --git a/docs/reference/func_config_envs_add.md b/docs/reference/func_envs_add.md similarity index 60% rename from docs/reference/func_config_envs_add.md rename to docs/reference/func_envs_add.md index 482d002b40..7365bc6b1c 100644 --- a/docs/reference/func_config_envs_add.md +++ b/docs/reference/func_envs_add.md @@ -1,10 +1,10 @@ -## func config envs add +## func envs add -Add environment variable to the function configuration +Add environment variable to the function ### Synopsis -Add environment variable to the function configuration. +Add environment variable to the function. If environment variable is not set explicitly by flag, interactive prompt is used. @@ -13,29 +13,29 @@ from an environment variable on the local machine or from Secrets and ConfigMaps It is also possible to import all keys as environment variables from a Secret or ConfigMap. ``` -func config envs add +func envs add ``` ### Examples ``` # set environment variable directly -func config envs add --name=VARNAME --value=myValue +func envs add --name=VARNAME --value=myValue # set environment variable from local env $LOC_ENV -func config envs add --name=VARNAME --value='{{ env:LOC_ENV }}' +func envs add --name=VARNAME --value='{{ env:LOC_ENV }}' set environment variable from a secret -func config envs add --name=VARNAME --value='{{ secret:secretName:key }}' +func envs add --name=VARNAME --value='{{ secret:secretName:key }}' # set all key as environment variables from a secret -func config envs add --value='{{ secret:secretName }}' +func envs add --value='{{ secret:secretName }}' # set environment variable from a configMap -func config envs add --name=VARNAME --value='{{ configMap:confMapName:key }}' +func envs add --name=VARNAME --value='{{ configMap:confMapName:key }}' # set all key as environment variables from a configMap -func config envs add --value='{{ configMap:confMapName }}' +func envs add --value='{{ configMap:confMapName }}' ``` ### Options @@ -50,5 +50,5 @@ func config envs add --value='{{ configMap:confMapName }}' ### SEE ALSO -* [func config envs](func_config_envs.md) - List and manage configured environment variable for a function +* [func envs](func_envs.md) - Manage function environment variables diff --git a/docs/reference/func_config_envs_remove.md b/docs/reference/func_envs_remove.md similarity index 76% rename from docs/reference/func_config_envs_remove.md rename to docs/reference/func_envs_remove.md index b7e34aac10..17131218a5 100644 --- a/docs/reference/func_config_envs_remove.md +++ b/docs/reference/func_envs_remove.md @@ -1,4 +1,4 @@ -## func config envs remove +## func envs remove Remove environment variable from the function configuration @@ -11,7 +11,7 @@ in the current directory or from the directory specified with --path. ``` -func config envs remove +func envs remove ``` ### Options @@ -24,5 +24,5 @@ func config envs remove ### SEE ALSO -* [func config envs](func_config_envs.md) - List and manage configured environment variable for a function +* [func envs](func_envs.md) - Manage function environment variables diff --git a/docs/reference/func_git.md b/docs/reference/func_git.md new file mode 100644 index 0000000000..5b28f754ec --- /dev/null +++ b/docs/reference/func_git.md @@ -0,0 +1,29 @@ +## func git + +Manage Git configuration of a function + +### Synopsis + +Manage Git configuration of a function + +Prints Git configuration for a function project present in +the current directory or from the directory specified with --path. + + +``` +func git +``` + +### Options + +``` + -h, --help help for git + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - func manages Knative Functions +* [func git set](func_git_set.md) - Set Git settings in the function configuration + diff --git a/docs/reference/func_git_set.md b/docs/reference/func_git_set.md new file mode 100644 index 0000000000..b2b8ac4f13 --- /dev/null +++ b/docs/reference/func_git_set.md @@ -0,0 +1,41 @@ +## func git set + +Set Git settings in the function configuration + +### Synopsis + +Set Git settings in the function configuration + + Interactive prompt to set Git settings in the function project in the current + directory or from the directory specified with --path. + + +``` +func git set +``` + +### Options + +``` + -b, --builder string Builder to use when creating the function's container. Currently supported builders are "pack" and "s2i". (default "pack") + --builder-image string Specify a custom builder image for use by the builder other than its default. ($FUNC_BUILDER_IMAGE) + --config-cluster Configure cluster resources (credentials and config on cluster). (default true) + --config-local Configure local resources (pipeline templates). (default true) + --config-remote Configure remote resources (webhook on the Git provider side). (default true) + --gh-access-token string GitHub Personal Access Token. For public repositories the scope is 'public_repo', for private is 'repo'. If you want to configure the webhook automatically, 'admin:repo_hook' is needed as well. Get more details: https://pipelines-as-code.pages.dev/docs/install/github_webhook/. + --gh-webhook-secret string GitHub Webhook Secret used for payload validation. If not specified, it will be generated automatically. + -t, --git-branch string Git revision (branch) to be used when deploying via the Git repository (Env: $FUNC_GIT_BRANCH) + -d, --git-dir string Directory in the Git repository containing the function (default is the root) (Env: $FUNC_GIT_DIR) + -g, --git-url string Repository url containing the function to build (Env: $FUNC_GIT_URL) + -h, --help help for set + -i, --image string Full image name in the form [registry]/[namespace]/[name]:[tag]@[digest]. This option takes precedence over --registry. Specifying digest is optional, but if it is given, 'build' and 'push' phases are disabled. (Env: $FUNC_IMAGE) + -n, --namespace string Deploy into a specific namespace. Will use function's current namespace by default if already deployed, and the currently active namespace if it can be determined. (Env: $FUNC_NAMESPACE) + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + -r, --registry string Container registry + registry namespace. (ex 'ghcr.io/myuser'). The full image name is automatically determined using this along with function name. (Env: $FUNC_REGISTRY) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func git](func_git.md) - Manage Git configuration of a function + diff --git a/docs/reference/func_info.md b/docs/reference/func_info.md deleted file mode 100644 index 6565ea8ac2..0000000000 --- a/docs/reference/func_info.md +++ /dev/null @@ -1,47 +0,0 @@ -## func info - -Show details of a function - -### Synopsis - -Show details of a function - -Prints the name, route and any event subscriptions for a deployed function in -the current directory or from the directory specified with --path. - - -``` -func info -``` - -### Examples - -``` - -# Show the details of a function as declared in the local func.yaml -func info - -# Show the details of the function in the myotherfunc directory with yaml output -func info --output yaml --path myotherfunc - -``` - -### Options - -``` - -h, --help help for info - -n, --namespace string The namespace in which to look for the named function. (Env: $FUNC_NAMESPACE) - -o, --output string Output format (human|plain|json|xml|yaml|url) (Env: $FUNC_OUTPUT) (default "human") - -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") -``` - -### Options inherited from parent commands - -``` - -v, --verbose Print verbose logs ($FUNC_VERBOSE) -``` - -### SEE ALSO - -* [func](func.md) - Serverless functions - diff --git a/docs/reference/func_labels.md b/docs/reference/func_labels.md new file mode 100644 index 0000000000..197f8330a9 --- /dev/null +++ b/docs/reference/func_labels.md @@ -0,0 +1,30 @@ +## func labels + +Manage function labels + +### Synopsis + +func labels + +Manages function labels. Default is to list currently configured labels. See +subcommands 'add' and 'remove'. + +``` +func labels +``` + +### Options + +``` + -h, --help help for labels + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - func manages Knative Functions +* [func labels add](func_labels_add.md) - Add label to the function +* [func labels remove](func_labels_remove.md) - Remove labels from the function configuration + diff --git a/docs/reference/func_labels_add.md b/docs/reference/func_labels_add.md new file mode 100644 index 0000000000..a05279ff76 --- /dev/null +++ b/docs/reference/func_labels_add.md @@ -0,0 +1,43 @@ +## func labels add + +Add label to the function + +### Synopsis + +Add labels to the function + +Add labels to the function project in the current directory or from the +directory specified with --path. If no flags are provided, addition is +completed using interactive prompts. + +The label can be set directly from a value or from an environment variable on +the local machine. + +``` +func labels add +``` + +### Examples + +``` +# add a label +func labels add --key=myLabel --value=myValue + +# add a label from a local environment variable +func labels add --key=myLabel --value='{{ env:LOC_ENV }}' +``` + +### Options + +``` + -h, --help help for add + --key string Key of the label. + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + --value string Value of the label. + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func labels](func_labels.md) - Manage function labels + diff --git a/docs/reference/func_config_labels_remove.md b/docs/reference/func_labels_remove.md similarity index 75% rename from docs/reference/func_config_labels_remove.md rename to docs/reference/func_labels_remove.md index 4114232c85..552d95ddd4 100644 --- a/docs/reference/func_config_labels_remove.md +++ b/docs/reference/func_labels_remove.md @@ -1,4 +1,4 @@ -## func config labels remove +## func labels remove Remove labels from the function configuration @@ -11,7 +11,7 @@ directory or from the directory specified with --path. ``` -func config labels remove +func labels remove ``` ### Options @@ -24,5 +24,5 @@ func config labels remove ### SEE ALSO -* [func config labels](func_config_labels.md) - List and manage configured labels for a function +* [func labels](func_labels.md) - Manage function labels diff --git a/docs/reference/func_volumes.md b/docs/reference/func_volumes.md new file mode 100644 index 0000000000..c538808eef --- /dev/null +++ b/docs/reference/func_volumes.md @@ -0,0 +1,31 @@ +## func volumes + +Manage function volumes + +### Synopsis + +func volumes + +Manges function volumes mounts. Default is to list currently configured +volumes. See subcommands 'add' and 'remove'. + + +``` +func volumes +``` + +### Options + +``` + -h, --help help for volumes + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - func manages Knative Functions +* [func volumes add](func_volumes_add.md) - Add volume to the function +* [func volumes remove](func_volumes_remove.md) - Remove volume from the function configuration + diff --git a/docs/reference/func_volumes_add.md b/docs/reference/func_volumes_add.md new file mode 100644 index 0000000000..94b7638c79 --- /dev/null +++ b/docs/reference/func_volumes_add.md @@ -0,0 +1,34 @@ +## func volumes add + +Add volume to the function + +### Synopsis + +Add volume to the function + +Add secrets and config maps as volume mounts to the function in the current +directory or from the directory specified by --path. If no flags are provided, +addition is completed using interactive prompts. + +The volume can be set + + +``` +func volumes add +``` + +### Options + +``` + --configmap string Name of the config map to mount. + -h, --help help for add + --mount string Mount path. + -p, --path string Path to the function. Default is current directory (Env: $FUNC_PATH) + --secret string Name of the secret to mount. + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func volumes](func_volumes.md) - Manage function volumes + diff --git a/docs/reference/func_config_volumes_remove.md b/docs/reference/func_volumes_remove.md similarity index 75% rename from docs/reference/func_config_volumes_remove.md rename to docs/reference/func_volumes_remove.md index d4fbffe4b4..93988064a5 100644 --- a/docs/reference/func_config_volumes_remove.md +++ b/docs/reference/func_volumes_remove.md @@ -1,4 +1,4 @@ -## func config volumes remove +## func volumes remove Remove volume from the function configuration @@ -11,7 +11,7 @@ in the current directory or from the directory specified with --path. ``` -func config volumes remove +func volumes remove ``` ### Options @@ -24,5 +24,5 @@ func config volumes remove ### SEE ALSO -* [func config volumes](func_config_volumes.md) - List and manage configured volumes for a function +* [func volumes](func_volumes.md) - Manage function volumes diff --git a/pkg/config/config.go b/pkg/config/config.go index 780a9b857f..c5a5184b0a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,6 +28,9 @@ const ( // DefaultBuilder is statically defined by the builders package. DefaultBuilder = builders.Default + + // DefaultOutput is human-optimized. + DefaultOutput = "human" ) // DefaultNamespace for remote operations is the currently active @@ -51,6 +54,7 @@ type Global struct { Confirm bool `yaml:"confirm,omitempty"` Language string `yaml:"language,omitempty"` Namespace string `yaml:"namespace,omitempty"` + Output string `yaml:"output,omitempty"` Registry string `yaml:"registry,omitempty"` Verbose bool `yaml:"verbose,omitempty"` // NOTE: all members must include their yaml serialized names, even when @@ -64,6 +68,7 @@ func New() Global { return Global{ Builder: DefaultBuilder, Language: DefaultLanguage, + Output: DefaultOutput, // ... } } @@ -137,15 +142,15 @@ func (c Global) Apply(f fn.Function) Global { if f.Build.Builder != "" { c.Builder = f.Build.Builder } - if f.Runtime != "" { - c.Language = f.Runtime - } if f.Deploy.Namespace != "" { c.Namespace = f.Deploy.Namespace } if f.Registry != "" { c.Registry = f.Registry } + if f.Runtime != "" { + c.Language = f.Runtime + } return c } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 60f54b70c6..dff47eff0a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -400,6 +400,7 @@ func TestList(t *testing.T) { "confirm", "language", "namespace", + "output", "registry", "verbose", } From 1178c72b43cb6a582ae547ad00a7c0a61314f837 Mon Sep 17 00:00:00 2001 From: Luke Kingland Date: Tue, 14 Mar 2023 22:52:45 +0900 Subject: [PATCH 2/2] feat: global config cli --- cmd/config.go | 397 ++++++++++++++++++++++++++++ cmd/config_test.go | 95 +++++++ cmd/envs.go | 3 +- cmd/metadata.go | 2 +- cmd/repository.go | 5 +- cmd/root.go | 9 +- docs/reference/func.md | 1 + docs/reference/func_config.md | 94 +++++++ docs/reference/func_config_get.md | 21 ++ docs/reference/func_config_list.md | 21 ++ docs/reference/func_config_set.md | 21 ++ docs/reference/func_config_unset.md | 21 ++ pkg/config/config.go | 14 +- pkg/config/config_test.go | 27 +- 14 files changed, 715 insertions(+), 16 deletions(-) create mode 100644 cmd/config.go create mode 100644 cmd/config_test.go create mode 100644 docs/reference/func_config.md create mode 100644 docs/reference/func_config_get.md create mode 100644 docs/reference/func_config_list.md create mode 100644 docs/reference/func_config_set.md create mode 100644 docs/reference/func_config_unset.md diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000000..68e383aefa --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,397 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/ory/viper" + "github.com/spf13/cobra" + "knative.dev/func/pkg/config" +) + +func NewConfigCmd(newClient ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Short: "Manage global system settings", + Use: "config [list|get|set|unset]", + Long: ` +NAME + {{rootCmdUse}} - Manage global configuration + +SYNOPSIS + {{rootCmdUse}} config list [-o|--output] [-c|--confirm] [-v|--verbose] + {{rootCmdUse}} config get [-o|--output] [-c|--confirm] [-v|--verbose] + {{rootCmdUse}} config set [-o|--output] [-c|--confirm] [-v|--verbose] + {{rootCmdUse}} config unset [-o|--output] [-c|--confirm] [-v|--verbose] + +DESCRIPTION + + Manage global configuration by listing current configuration, and either + retreiving individual settings with 'get' or setting with 'set'. + Values set will become the new default for all function commands. + + Settings are stored in ~/.config/func/config.yaml by default, but whose path + can be altered using the XDG_CONFIG_HOME environment variable (by default + ~/.config). + + A specific global config file can also be used by specifying the environment + variable FUNC_CONFIG_FILE, which takes highest precidence. + + Values defined in this global configuration are only defaults. If a given + function has a defined value for the option, that will be used. This value + can also be superceded with environment variables or command line flags. To + see the final value of an option that will be used for a given command, see + the given command's help text. + +COMMANDS + + With no arguments, this help text is shown. To manage global configuration + using an interactive prompt, use the --confirm (-c) flag. + $ {{rootCmdUse}} config -c + + list + List all global configuration options and their current values. + + get + Get the current value of a named global configuration option. + + set + Set the named global configuration option to the value provided. + + unset + Remove any user-provided setting for the given option, resetting it to the + original default. + +EXAMPLES + o List all configuration options and their current values + $ {{rootCmdUse}} config list + + o Set a new global default value for Reggistry + $ {{rootCmdUse}} config set registry registry.example.com/bob + + o Get the current default value for Registry + $ {{rootCmdUse}} config get registry + registry.example.com/bob + + o Unset the custom global registry default, reverting it to the static + default (aka factory default). In the case of registry, there is no + static default, so unsetting the default will result in the 'deploy' + command requiring it to be provided when invoked. + $ {{rootCmdUse}} config unset registry + +`, + SuggestFor: []string{"cnofig", "donfig", "vonfig", "xonfig", "cinfig", "clnfig", "cpnfig"}, + PreRunE: bindEnv("confirm", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigCmd(cmd, args, newClient) + }, + } + + cfg, err := config.NewDefault() + if err != nil { + fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err) + } + + addConfirmFlag(cmd, cfg.Confirm) + addOutputFlag(cmd, cfg.Output) + addVerboseFlag(cmd, cfg.Verbose) + + cmd.AddCommand(NewConfigListCmd(newClient, cfg)) + cmd.AddCommand(NewConfigGetCmd(newClient, cfg)) + cmd.AddCommand(NewConfigSetCmd(newClient, cfg)) + cmd.AddCommand(NewConfigUnsetCmd(newClient, cfg)) + + return cmd +} + +func NewConfigListCmd(newClient ClientFactory, cfg config.Global) *cobra.Command { + cmd := &cobra.Command{ + Short: "List global configuration options", + Use: "list", + PreRunE: bindEnv("confirm", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigList(cmd, args, newClient) + }, + } + addConfirmFlag(cmd, cfg.Confirm) + addOutputFlag(cmd, cfg.Output) + addVerboseFlag(cmd, cfg.Verbose) + return cmd +} + +func NewConfigGetCmd(newClient ClientFactory, cfg config.Global) *cobra.Command { + cmd := &cobra.Command{ + Short: "Get a global configuration option", + Use: "get ", + PreRunE: bindEnv("confirm", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigGet(cmd, args, newClient) + }, + } + addConfirmFlag(cmd, cfg.Confirm) + addOutputFlag(cmd, cfg.Output) + addVerboseFlag(cmd, cfg.Verbose) + return cmd +} + +func NewConfigSetCmd(newClient ClientFactory, cfg config.Global) *cobra.Command { + cmd := &cobra.Command{ + Short: "Get a global configuration option", + Use: "set ", + PreRunE: bindEnv("confirm", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigSet(cmd, args, newClient) + }, + } + addConfirmFlag(cmd, cfg.Confirm) + addOutputFlag(cmd, cfg.Output) + addVerboseFlag(cmd, cfg.Verbose) + return cmd +} + +func NewConfigUnsetCmd(newClient ClientFactory, cfg config.Global) *cobra.Command { + cmd := &cobra.Command{ + Short: "Get a global configuration option", + Use: "unset ", + PreRunE: bindEnv("confirm", "output", "verbose"), + RunE: func(cmd *cobra.Command, args []string) error { + return runConfigUnset(cmd, args, newClient) + }, + } + addConfirmFlag(cmd, cfg.Confirm) + addOutputFlag(cmd, cfg.Output) + addVerboseFlag(cmd, cfg.Verbose) + return cmd +} + +func runConfigCmd(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + var cfg configConfig // global configuration settings command config + if cfg, err = newConfigConfig("", args).Prompt(); err != nil { + return + } + + if cfg.Action == "" { + return cmd.Help() + } + + switch cfg.Action { + case "list": + return runConfigList(cmd, args, newClient) + case "get": + return runConfigGet(cmd, args, newClient) + case "set": + return runConfigSet(cmd, args, newClient) + case "unset": + return runConfigUnset(cmd, args, newClient) + default: + return fmt.Errorf("invalid action '%v'", cfg.Action) + } +} + +func runConfigList(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + var cfg configConfig + if cfg, err = newConfigConfig("list", args).Prompt(); err != nil { + return + } + + // Global Settings to Query + settings, err := config.NewDefault() + if err != nil { + return fmt.Errorf("error loading config at '%v'. %v\n", config.File(), err) + } + + switch Format(cfg.Output) { + case Human: + for _, v := range config.List() { + fmt.Fprintf(cmd.OutOrStdout(), "%v=%v\n", v, config.Get(settings, v)) + } + return + case JSON: + var bb []byte + bb, err = json.MarshalIndent(settings, "", " ") + fmt.Fprintln(cmd.OutOrStdout(), string(bb)) + return + default: + return fmt.Errorf("invalid format: %v", cfg.Output) + } +} + +func runConfigGet(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + // Config + var cfg configConfig + if cfg, err = newConfigConfig("get", args).Prompt(); err != nil { + return + } + + // Preconditions + if cfg.Name == "" { + return fmt.Errorf("Setting name is requred: get ") + } + + // Global Settings to Query + settings, err := config.NewDefault() + if err != nil { + return fmt.Errorf("error loading config at '%v'. %v\n", config.File(), err) + } + + // Output named attribute value as output type + value := config.Get(settings, cfg.Name) + switch Format(cfg.Output) { + case Human: + fmt.Fprintf(cmd.OutOrStdout(), "%v\n", value) + return + case JSON: + var bb []byte + bb, err = json.MarshalIndent(value, "", " ") + fmt.Fprintln(cmd.OutOrStdout(), string(bb)) + return + default: + return fmt.Errorf("invalid format: %v", cfg.Output) + } + + return nil +} + +func runConfigSet(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + // Config + var cfg configConfig + if cfg, err = newConfigConfig("set", args).Prompt(); err != nil { + return + } + + // Preconditions + if cfg.Name == "" { + return fmt.Errorf("Setting name is requred: set ") + } + if cfg.Value == "" { + return fmt.Errorf("Setting value is requred: set ") + } + + // Global Settings to Mutate + settings, err := config.NewDefault() + if err != nil { + return fmt.Errorf("error loading config at '%v'. %v\n", config.File(), err) + } + + // Set + settings, err = config.Set(settings, cfg.Name, cfg.Value) + if err != nil { + return + } + return settings.Write(config.File()) +} + +func runConfigUnset(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) { + // Config + var cfg configConfig + if cfg, err = newConfigConfig("unset", args).Prompt(); err != nil { + return + } + + // Preconditions + if cfg.Name == "" { + return fmt.Errorf("Setting name is requred: unset ") + } + + defaults := config.New() // static defaults + settings, err := config.NewDefault() // customized + if err != nil { + return fmt.Errorf("error loading config at '%v'. %v\n", config.File(), err) + } + + // Reset + // The setter should alwasy be able to accept the string serialized value + // of the default value returned. + defaultValue := config.Get(defaults, cfg.Name) + settings, err = config.Set(settings, cfg.Name, fmt.Sprintf("%s", defaultValue)) + if err != nil { + return + } + return settings.Write(config.File()) +} + +type configConfig struct { + config.Global + Action string + Name string + Value string +} + +func newConfigConfig(action string, args []string) configConfig { + cfg := configConfig{ + Global: config.Global{ + Confirm: viper.GetBool("confirm"), + Verbose: viper.GetBool("verbose"), + Output: viper.GetString("output"), + }, + Action: action, + } + + if action != "" { + cfg.Action = action + if len(args) > 0 { + cfg.Name = args[0] + } + if len(args) > 1 { + cfg.Value = args[1] + } + } + + return cfg +} + +func (c configConfig) Prompt() (configConfig, error) { + if !interactiveTerminal() || !c.Confirm { + return c, nil + } + fmt.Printf("Prompt with c.Action =%v, c.Name=%v, c.Value=%v\n", c.Action, c.Name, c.Value) + + // TODO: validators + + // If action is not explicitly provided, this is the root command asking + // which action to perform. + if c.Action == "" { + qs := []*survey.Question{{ + Name: "Action", + Prompt: &survey.Select{ + Message: "Operation to perform:", + Options: []string{"list", "get", "set", "unset"}, + Default: "list", + }}} + if err := survey.Ask(qs, &c); err != nil { + return c, err + } + // The only way action can be empty is if we're configuring via the + // root 'config' command, so just return. the subcommand invoked + // will re-run prompt to get the remaining values. + return c, nil + } + + // Prompt for 'name' if the action is 'get','set' or 'unset' + if c.Name == "" && (c.Action == "get" || c.Action == "set" || c.Action == "unset") { + qs := []*survey.Question{{ + Name: "Name", + Prompt: &survey.Input{ + Message: "Name of Global Default:", + Default: c.Name, + }}} + if err := survey.Ask(qs, &c); err != nil { + return c, err + } + } + + // Prompt for 'value' the action is a 'set' + if c.Value == "" && c.Action == "set" { + qs := []*survey.Question{{ + Name: "Value", + Prompt: &survey.Input{ + Message: "Global Default Value:", + Default: c.Value, + }}} + if err := survey.Ask(qs, &c); err != nil { + return c, err + } + } + + return c, nil +} diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000000..a7cf3d2557 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,95 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + "knative.dev/func/pkg/config" +) + +// TestConfig_List ensures that the 'list' subcommand shows all configurable +// global settings along with their current values. +func TestConfig_List(t *testing.T) { + root := fromTempDirectory(t) + t.Setenv("XDG_CONFIG_HOME", root) + + // Create a test Global Config with a customized value for registry + os.Mkdir(filepath.Join(root, "func"), os.ModePerm) + cfg := config.Global{ + Registry: "registry.example.com/alice", + } + cfg.Write(config.File()) + + // Ensure the list subcommand picks it up. + cmd := NewConfigCmd(NewClient) + cmd.SetArgs([]string{"list", "-o=json"}) + + buff := bytes.Buffer{} + cmd.SetOut(&buff) + cmd.SetErr(&buff) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + output := struct { + Registry string + }{} + json.Unmarshal(buff.Bytes(), &output) + if output.Registry != "registry.example.com/alice" { + t.Fatalf("did not receive expected config registry. got '%v'", output.Registry) + } + + // Note that fromTempDirectory completely clears the current environment + // such that the settings from the user running tests do not interfere + // (all environment variables are cleared), and thus the config current + // values will all be set to their static code deafults in the config package. + // if !strings.Contains(buff.String(), "repository") + +} + +// TestConfig_Set ensures that the 'set' config subcommand results in a new +// global config value being set. +func TestConfig_Add(t *testing.T) { + root := fromTempDirectory(t) + t.Setenv("XDG_CONFIG_HOME", root) + if err := config.CreatePaths(); err != nil { + t.Fatal(err) + } + + // Add a String + cmd := NewConfigCmd(NewClient) + cmd.SetArgs([]string{"set", "registry", "registry.example.com/bob"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + cfg, err := config.NewDefault() + if err != nil { + t.Fatal(err) + } + + if cfg.Registry != "registry.example.com/bob" { + t.Fatalf("unexpected global config registry value: %v", cfg.Registry) + } + + // Add a Boolean + cmd = NewConfigCmd(NewClient) + cmd.SetArgs([]string{"set", "verbose", "true"}) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + cfg, err = config.NewDefault() + if err != nil { + t.Fatal(err) + } + + if cfg.Verbose != true { + t.Fatalf("unexpected global config verbose value: %v", cfg.Registry) + } + +} diff --git a/cmd/envs.go b/cmd/envs.go index ffbdcc74c1..559cf895fc 100644 --- a/cmd/envs.go +++ b/cmd/envs.go @@ -85,8 +85,7 @@ func runEnvs(cmd *cobra.Command, newClient ClientFactory) (err error) { case JSON: return json.NewEncoder(cmd.OutOrStdout()).Encode(f.Run.Envs) default: - fmt.Fprintf(cmd.ErrOrStderr(), "invalid format: %v", cfg.Output) - return + return fmt.Errorf("invalid format: %v", cfg.Output) } } diff --git a/cmd/metadata.go b/cmd/metadata.go index 49b96dd6ee..b0c084a8c6 100644 --- a/cmd/metadata.go +++ b/cmd/metadata.go @@ -26,7 +26,7 @@ func newMetadataConfig() metadataConfig { func (c metadataConfig) Validate() error { // TODO: validate cascate upwards like the constructor. - // c.Global.Validat() // should validate .Output + // c.Global.Validate() // should validate .Output return nil } diff --git a/cmd/repository.go b/cmd/repository.go index 696eebc430..ccf958be74 100644 --- a/cmd/repository.go +++ b/cmd/repository.go @@ -13,9 +13,6 @@ import ( fn "knative.dev/func/pkg/functions" ) -// command constructors -// -------------------- - func NewRepositoryCmd(newClient ClientFactory) *cobra.Command { cmd := &cobra.Command{ Short: "Manage installed template repositories", @@ -263,6 +260,8 @@ func runRepository(cmd *cobra.Command, args []string, newClient ClientFactory) ( return } + // TODO: move the below to repositoryConfig.Prompt() + // If in noninteractive, normal mode the help text is shown if !cfg.Confirm { return cmd.Help() diff --git a/cmd/root.go b/cmd/root.go index 32033c8ece..64e42e8ba9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -95,7 +95,7 @@ Learn more about Knative at: https://knative.dev`, cfg.Name), { Header: "System Settings:", Commands: []*cobra.Command{ - // NewConfigCmd(defaultLoaderSaver), // NOTE: Global Config being added in a separate PR + NewConfigCmd(newClient), NewLanguagesCmd(newClient), NewTemplatesCmd(newClient), NewRepositoryCmd(newClient), @@ -316,11 +316,16 @@ func mergeEnvs(envs []fn.Env, envToUpdate *util.OrderedMap, envToRemove []string return envs, counter, nil } -// addConfirmFlag ensures common text/wording when the --path flag is used +// addConfirmFlag ensures common text.wording when the --confirm flag is used func addConfirmFlag(cmd *cobra.Command, dflt bool) { cmd.Flags().BoolP("confirm", "c", dflt, "Prompt to confirm options interactively (Env: $FUNC_CONFIRM)") } +// addOutputFlag ensures common text.wording when the --output flag is used +func addOutputFlag(cmd *cobra.Command, dflt string) { + cmd.Flags().StringP("output", "o", dflt, "Output format (human|json) (Env: $FUNC_OUTPUT)") +} + // addPathFlag ensures common text/wording when the --path flag is used func addPathFlag(cmd *cobra.Command) { cmd.Flags().StringP("path", "p", "", "Path to the function. Default is current directory (Env: $FUNC_PATH)") diff --git a/docs/reference/func.md b/docs/reference/func.md index 9291c0e253..1e45a5558f 100644 --- a/docs/reference/func.md +++ b/docs/reference/func.md @@ -25,6 +25,7 @@ Learn more about Knative at: https://knative.dev * [func build](func_build.md) - Build a function container * [func completion](func_completion.md) - Output functions shell completion code +* [func config](func_config.md) - Manage global system settings * [func create](func_create.md) - Create a function * [func delete](func_delete.md) - Undeploy a function * [func deploy](func_deploy.md) - Deploy a function diff --git a/docs/reference/func_config.md b/docs/reference/func_config.md new file mode 100644 index 0000000000..6ff89f07fa --- /dev/null +++ b/docs/reference/func_config.md @@ -0,0 +1,94 @@ +## func config + +Manage global system settings + +### Synopsis + + +NAME + func - Manage global configuration + +SYNOPSIS + func config list [-o|--output] [-c|--confirm] [-v|--verbose] + func config get [-o|--output] [-c|--confirm] [-v|--verbose] + func config set [-o|--output] [-c|--confirm] [-v|--verbose] + func config unset [-o|--output] [-c|--confirm] [-v|--verbose] + +DESCRIPTION + + Manage global configuration by listing current configuration, and either + retreiving individual settings with 'get' or setting with 'set'. + Values set will become the new default for all function commands. + + Settings are stored in ~/.config/func/config.yaml by default, but whose path + can be altered using the XDG_CONFIG_HOME environment variable (by default + ~/.config). + + A specific global config file can also be used by specifying the environment + variable FUNC_CONFIG_FILE, which takes highest precidence. + + Values defined in this global configuration are only defaults. If a given + function has a defined value for the option, that will be used. This value + can also be superceded with environment variables or command line flags. To + see the final value of an option that will be used for a given command, see + the given command's help text. + +COMMANDS + + With no arguments, this help text is shown. To manage global configuration + using an interactive prompt, use the --confirm (-c) flag. + $ func config -c + + list + List all global configuration options and their current values. + + get + Get the current value of a named global configuration option. + + set + Set the named global configuration option to the value provided. + + unset + Remove any user-provided setting for the given option, resetting it to the + original default. + +EXAMPLES + o List all configuration options and their current values + $ func config list + + o Set a new global default value for Reggistry + $ func config set registry registry.example.com/bob + + o Get the current default value for Registry + $ func config get registry + registry.example.com/bob + + o Unset the custom global registry default, reverting it to the static + default (aka factory default). In the case of registry, there is no + static default, so unsetting the default will result in the 'deploy' + command requiring it to be provided when invoked. + $ func config unset registry + + + +``` +func config [list|get|set|unset] +``` + +### Options + +``` + -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) + -h, --help help for config + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func](func.md) - func manages Knative Functions +* [func config get](func_config_get.md) - Get a global configuration option +* [func config list](func_config_list.md) - List global configuration options +* [func config set](func_config_set.md) - Get a global configuration option +* [func config unset](func_config_unset.md) - Get a global configuration option + diff --git a/docs/reference/func_config_get.md b/docs/reference/func_config_get.md new file mode 100644 index 0000000000..88c593246c --- /dev/null +++ b/docs/reference/func_config_get.md @@ -0,0 +1,21 @@ +## func config get + +Get a global configuration option + +``` +func config get +``` + +### Options + +``` + -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) + -h, --help help for get + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func config](func_config.md) - Manage global system settings + diff --git a/docs/reference/func_config_list.md b/docs/reference/func_config_list.md new file mode 100644 index 0000000000..b1528f3cad --- /dev/null +++ b/docs/reference/func_config_list.md @@ -0,0 +1,21 @@ +## func config list + +List global configuration options + +``` +func config list +``` + +### Options + +``` + -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) + -h, --help help for list + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func config](func_config.md) - Manage global system settings + diff --git a/docs/reference/func_config_set.md b/docs/reference/func_config_set.md new file mode 100644 index 0000000000..1c3a24a9f9 --- /dev/null +++ b/docs/reference/func_config_set.md @@ -0,0 +1,21 @@ +## func config set + +Get a global configuration option + +``` +func config set +``` + +### Options + +``` + -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) + -h, --help help for set + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func config](func_config.md) - Manage global system settings + diff --git a/docs/reference/func_config_unset.md b/docs/reference/func_config_unset.md new file mode 100644 index 0000000000..e6b8fd4180 --- /dev/null +++ b/docs/reference/func_config_unset.md @@ -0,0 +1,21 @@ +## func config unset + +Get a global configuration option + +``` +func config unset +``` + +### Options + +``` + -c, --confirm Prompt to confirm options interactively (Env: $FUNC_CONFIRM) + -h, --help help for unset + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -v, --verbose Print verbose logs ($FUNC_VERBOSE) +``` + +### SEE ALSO + +* [func config](func_config.md) - Manage global system settings + diff --git a/pkg/config/config.go b/pkg/config/config.go index c5a5184b0a..f1d741af37 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -50,13 +50,13 @@ func DefaultNamespace() (namespace string) { // Global configuration settings. type Global struct { - Builder string `yaml:"builder,omitempty"` - Confirm bool `yaml:"confirm,omitempty"` - Language string `yaml:"language,omitempty"` - Namespace string `yaml:"namespace,omitempty"` - Output string `yaml:"output,omitempty"` - Registry string `yaml:"registry,omitempty"` - Verbose bool `yaml:"verbose,omitempty"` + Builder string `yaml:"builder,omitempty"json:",omitempty"` + Confirm bool `yaml:"confirm,omitempty"json:",omitempty"` + Language string `yaml:"language,omitempty"json:",omitempty"` + Namespace string `yaml:"namespace,omitempty"json:",omitempty"` + Output string `yaml:"output,omitempty"json:",omitempty"` + Registry string `yaml:"registry,omitempty"json:",omitempty"` + Verbose bool `yaml:"verbose,omitempty"json:",omitempty"` // NOTE: all members must include their yaml serialized names, even when // this is the default, because these tag values are used for the static // getter/setter accessors to match requests. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index dff47eff0a..ae78ecdb59 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -328,7 +328,8 @@ func TestGet_Valid(t *testing.T) { // TestSet_Invalid ensures that attemptint to set an invalid field errors. func TestSet_Invalid(t *testing.T) { - _, err := config.SetString(config.Global{}, "invalid", "foo") + cfg := config.Global{} + cfg, err := config.SetString(cfg, "invalid", "foo") if err == nil { t.Fatal("did not receive expected error setting a nonexistent field") } @@ -361,6 +362,30 @@ func TestSet_ValidTyped(t *testing.T) { // include types of additional values. } +// TestSet_Zero ensures that setting a value to its zero value is respected +func TestSet_Zero(t *testing.T) { + cfg := config.Global{} + + // Set a String from a string + // should be the base case + cfg, err := config.Set(cfg, "language", "go") + if err != nil { + t.Fatal(err) + } + if cfg.Language != "go" { + t.Fatalf("unexpected value for config language: %v", cfg.Language) + } + + // Set to the zero value via String + cfg, err = config.Set(cfg, "language", "") + if err != nil { + t.Fatal(err) + } + if cfg.Language != "" { + t.Fatalf("unexpected value for config builder: %v", cfg.Builder) + } +} + // TestSet_ValidStrings ensures that setting valid attribute names using // the string representation of their values succeeds. func TestSet_ValidStrings(t *testing.T) {