From 6ba3cf3f6bc8641542dd4a919b0ae688bdbb7aef Mon Sep 17 00:00:00 2001 From: Matej Vasek Date: Wed, 24 Aug 2022 17:10:03 +0200 Subject: [PATCH] Improve `func config envs` * Added ability to add env non-interactively * Added ability to list envs as JSON Signed-off-by: Matej Vasek --- cmd/config.go | 27 ++-- cmd/config_envs.go | 110 +++++++++++---- cmd/config_labels.go | 6 +- cmd/config_test.go | 182 +++++++++++++++++++++++++ cmd/config_volumes.go | 6 +- cmd/root.go | 2 +- docs/reference/func_config_envs.md | 5 +- docs/reference/func_config_envs_add.md | 35 ++++- 8 files changed, 319 insertions(+), 54 deletions(-) create mode 100644 cmd/config_test.go diff --git a/cmd/config.go b/cmd/config.go index 3d8731a2fb..421648c7d2 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -6,6 +6,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/ory/viper" "github.com/spf13/cobra" fn "knative.dev/kn-plugin-func" @@ -43,7 +44,7 @@ func (s standardLoaderSaver) Save(f fn.Function) error { var defaultLoaderSaver standardLoaderSaver -func NewConfigCmd() *cobra.Command { +func NewConfigCmd(loadSaver functionLoaderSaver) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Configure a function", @@ -61,8 +62,8 @@ or from the directory specified with --path. setPathFlag(cmd) - cmd.AddCommand(NewConfigLabelsCmd(defaultLoaderSaver)) - cmd.AddCommand(NewConfigEnvsCmd()) + cmd.AddCommand(NewConfigLabelsCmd(loadSaver)) + cmd.AddCommand(NewConfigEnvsCmd(loadSaver)) cmd.AddCommand(NewConfigVolumesCmd()) return cmd @@ -70,7 +71,7 @@ or from the directory specified with --path. func runConfigCmd(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(defaultLoaderSaver) if err != nil { return } @@ -128,7 +129,7 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) { if answers.SelectedConfig == "Volumes" { listVolumes(function) } else if answers.SelectedConfig == "Environment variables" { - listEnvs(function) + err = listEnvs(function, cmd.OutOrStdout(), Human) } else if answers.SelectedConfig == "Labels" { listLabels(function) } @@ -141,25 +142,19 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) { // ------------------------------ type configCmdConfig struct { - Name string Path string Verbose bool } -func newConfigCmdConfig(args []string) configCmdConfig { - var name string - if len(args) > 0 { - name = args[0] - } +func newConfigCmdConfig() configCmdConfig { return configCmdConfig{ - Name: deriveName(name, getPathFlag()), - Path: getPathFlag(), + Path: getPathFlag(), + Verbose: viper.GetBool("verbose"), } - } -func initConfigCommand(args []string, loader functionLoader) (fn.Function, error) { - config := newConfigCmdConfig(args) +func initConfigCommand(loader functionLoader) (fn.Function, error) { + config := newConfigCmdConfig() function, err := loader.Load(config.Path) if err != nil { diff --git a/cmd/config_envs.go b/cmd/config_envs.go index da5403dc8d..b38ed79e93 100644 --- a/cmd/config_envs.go +++ b/cmd/config_envs.go @@ -2,12 +2,15 @@ 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" fn "knative.dev/kn-plugin-func" @@ -15,7 +18,7 @@ import ( "knative.dev/kn-plugin-func/utils" ) -func NewConfigEnvsCmd() *cobra.Command { +func NewConfigEnvsCmd(loadSaver functionLoaderSaver) *cobra.Command { cmd := &cobra.Command{ Use: "envs", Short: "List and manage configured environment variable for a function", @@ -25,20 +28,20 @@ Prints configured Environment variable for a function project present in the current directory or from the directory specified with --path. `, SuggestFor: []string{"ensv", "env"}, - PreRunE: bindEnv("path"), + PreRunE: bindEnv("path", "output"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(loadSaver) if err != nil { return } - listEnvs(function) - - return + return listEnvs(function, cmd.OutOrStdout(), Format(viper.GetString("output"))) }, } - configEnvsAddCmd := NewConfigEnvsAddCmd() + cmd.Flags().StringP("output", "o", "human", "Output format (human|json) (Env: $FUNC_OUTPUT)") + + configEnvsAddCmd := NewConfigEnvsAddCmd(loadSaver) configEnvsRemoveCmd := NewConfigEnvsRemoveCmd() setPathFlag(cmd) @@ -51,29 +54,78 @@ the current directory or from the directory specified with --path. return cmd } -func NewConfigEnvsAddCmd() *cobra.Command { +func NewConfigEnvsAddCmd(loadSaver functionLoaderSaver) *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Add environment variable to the function configuration", - Long: `Add environment variable to the function configuration + Long: `Add environment variable to the function configuration. -Interactive prompt to add Environment variables to the function project -in the current directory or from the directory specified with --path. +If environment variable is not set explicitly by flag, interactive prompt is used. 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 +{{.Name}} config envs add --name=VARNAME --value=myValue + +# set environment variable from local env $LOC_ENV +{{.Name}} config envs add --name=VARNAME --value='{{"{{"}} env:LOC_ENV {{"}}"}}' + +set environment variable from a secret +{{.Name}} config envs add --name=VARNAME --value='{{"{{"}} secret:secretName:key {{"}}"}}' + +# set all key as environment variables from a secret +{{.Name}} config envs add --value='{{"{{"}} secret:secretName {{"}}"}}' + +# set environment variable from a configMap +{{.Name}} config envs add --name=VARNAME --value='{{"{{"}} configMap:confMapName:key {{"}}"}}' + +# set all key as environment variables from a configMap +{{.Name}} config envs add --value='{{"{{"}} configMap:confMapName {{"}}"}}'`, SuggestFor: []string{"ad", "create", "insert", "append"}, - PreRunE: bindEnv("path"), + PreRunE: bindEnv("path", "name", "value"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + 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.Envs = append(function.Envs, fn.Env{Name: np, Value: vp}) + return loadSaver.Save(function) + } + return runAddEnvsPrompt(cmd.Context(), function) }, } + + cmd.Flags().StringP("name", "", "", "Name of the environment variable.") + cmd.Flags().StringP("value", "", "", "Value of the environment variable.") + cmd.SetHelpFunc(defaultTemplatedHelp) return cmd } @@ -90,7 +142,7 @@ in the current directory or from the directory specified with --path. SuggestFor: []string{"rm", "del", "delete", "rmeove"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(defaultLoaderSaver) if err != nil { return } @@ -101,15 +153,27 @@ in the current directory or from the directory specified with --path. } -func listEnvs(f fn.Function) { - if len(f.Envs) == 0 { - fmt.Println("There aren't any configured Environment variables") - return - } +func listEnvs(f fn.Function, w io.Writer, outputFormat Format) error { + switch outputFormat { + case Human: + if len(f.Envs) == 0 { + _, err := fmt.Fprintln(w, "There aren't any configured Environment variables") + return err + } - fmt.Println("Configured Environment variables:") - for _, e := range f.Envs { - fmt.Println(" - ", e.String()) + fmt.Println("Configured Environment variables:") + for _, e := range f.Envs { + _, err := fmt.Fprintln(w, " - ", e.String()) + if err != nil { + return err + } + } + return nil + case JSON: + enc := json.NewEncoder(w) + return enc.Encode(f.Envs) + default: + return fmt.Errorf("bad format: %v", outputFormat) } } diff --git a/cmd/config_labels.go b/cmd/config_labels.go index 7e3e75c09f..39da67ee55 100644 --- a/cmd/config_labels.go +++ b/cmd/config_labels.go @@ -25,7 +25,7 @@ the current directory or from the directory specified with --path. SuggestFor: []string{"albels", "abels", "label"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, loaderSaver) + function, err := initConfigCommand(loaderSaver) if err != nil { return } @@ -51,7 +51,7 @@ the local machine. SuggestFor: []string{"ad", "create", "insert", "append"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, loaderSaver) + function, err := initConfigCommand(loaderSaver) if err != nil { return } @@ -72,7 +72,7 @@ directory or from the directory specified with --path. SuggestFor: []string{"del", "delete", "rmeove"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, loaderSaver) + function, err := initConfigCommand(loaderSaver) if err != nil { return } diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000000..4deca4e524 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,182 @@ +package cmd_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "sort" + "testing" + + "github.com/ory/viper" + fn "knative.dev/kn-plugin-func" + fnCmd "knative.dev/kn-plugin-func/cmd" +) + +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{Envs: envs}, nil + } + + cmd := fnCmd.NewConfigCmd(mock) + 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{Envs: []fn.Env{{Name: &foo, Value: &bar}}}, nil + } + var expectedEnvs []fn.Env + mock.save = func(f fn.Function) error { + if !envsEqual(expectedEnvs, f.Envs) { + return fmt.Errorf("unexpected envs: got %v but %v was expected", f.Envs, expectedEnvs) + } + return nil + } + + expectedEnvs = []fn.Env{{Name: &foo, Value: &bar}, {Name: &answer, Value: &fortyTwo}} + cmd := fnCmd.NewConfigCmd(mock) + 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) + 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) + 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 index add04a3a72..83d65c1c54 100644 --- a/cmd/config_volumes.go +++ b/cmd/config_volumes.go @@ -25,7 +25,7 @@ the current directory or from the directory specified with --path. SuggestFor: []string{"volums", "volume", "vols"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(defaultLoaderSaver) if err != nil { return } @@ -62,7 +62,7 @@ in the current directory or from the directory specified with --path. SuggestFor: []string{"ad", "create", "insert", "append"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(defaultLoaderSaver) if err != nil { return } @@ -86,7 +86,7 @@ in the current directory or from the directory specified with --path. SuggestFor: []string{"del", "delete", "rmeove"}, PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { - function, err := initConfigCommand(args, defaultLoaderSaver) + function, err := initConfigCommand(defaultLoaderSaver) if err != nil { return } diff --git a/cmd/root.go b/cmd/root.go index 0d18294110..b45b159412 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -88,7 +88,7 @@ EXAMPLES } cmd.AddCommand(NewCreateCmd(newClient)) - cmd.AddCommand(NewConfigCmd()) + cmd.AddCommand(NewConfigCmd(defaultLoaderSaver)) cmd.AddCommand(NewBuildCmd(newClient)) cmd.AddCommand(NewDeployCmd(newClient)) cmd.AddCommand(NewDeleteCmd(newClient)) diff --git a/docs/reference/func_config_envs.md b/docs/reference/func_config_envs.md index 0fc62f07f4..df565ec7da 100644 --- a/docs/reference/func_config_envs.md +++ b/docs/reference/func_config_envs.md @@ -17,8 +17,9 @@ func config envs [flags] ### Options ``` - -h, --help help for envs - -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") + -h, --help help for envs + -o, --output string Output format (human|json) (Env: $FUNC_OUTPUT) (default "human") + -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") ``` ### Options inherited from parent commands diff --git a/docs/reference/func_config_envs_add.md b/docs/reference/func_config_envs_add.md index 6a2dae0bb3..f4972b45c2 100644 --- a/docs/reference/func_config_envs_add.md +++ b/docs/reference/func_config_envs_add.md @@ -4,24 +4,47 @@ Add environment variable to the function configuration ### Synopsis -Add environment variable to the function configuration +Add environment variable to the function configuration. -Interactive prompt to add Environment variables to the function project -in the current directory or from the directory specified with --path. +If environment variable is not set explicitly by flag, interactive prompt is used. 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. ``` func config envs add [flags] ``` +### Examples + +``` +# set environment variable directly +func config envs add --name=VARNAME --value=myValue + +# set environment variable from local env $LOC_ENV +func config envs add --name=VARNAME --value='{{ env:LOC_ENV }}' + +set environment variable from a secret +func config envs add --name=VARNAME --value='{{ secret:secretName:key }}' + +# set all key as environment variables from a secret +func config envs add --value='{{ secret:secretName }}' + +# set environment variable from a configMap +func config envs add --name=VARNAME --value='{{ configMap:confMapName:key }}' + +# set all key as environment variables from a configMap +func config envs add --value='{{ configMap:confMapName }}' +``` + ### Options ``` - -h, --help help for add - -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") + -h, --help help for add + --name string Name of the environment variable. + -p, --path string Path to the project directory (Env: $FUNC_PATH) (default ".") + --value string Value of the environment variable. ``` ### Options inherited from parent commands