diff --git a/cmd/config.go b/cmd/config.go index b3fa74dd6..213472ad0 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -72,14 +72,20 @@ func runConfigCmd(cmd *cobra.Command, args []string) (err error) { case "Add": if answers.SelectedConfig == "Volumes" { err = runAddVolumesPrompt(cmd.Context(), function) + } else if answers.SelectedConfig == "Environment values" { + err = runAddEnvsPrompt(cmd.Context(), function) } case "Remove": if answers.SelectedConfig == "Volumes" { err = runRemoveVolumesPrompt(function) + } else if answers.SelectedConfig == "Environment values" { + err = runRemoveEnvsPrompt(function) } case "List": if answers.SelectedConfig == "Volumes" { listVolumes(function) + } else if answers.SelectedConfig == "Environment values" { + listEnvs(function) } } diff --git a/cmd/config_envs.go b/cmd/config_envs.go new file mode 100644 index 000000000..576460c90 --- /dev/null +++ b/cmd/config_envs.go @@ -0,0 +1,425 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/spf13/cobra" + + bosonFunc "github.com/boson-project/func" + "github.com/boson-project/func/k8s" + "github.com/boson-project/func/utils" +) + +func init() { + configCmd.AddCommand(configEnvsCmd) + configEnvsCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)") + configEnvsCmd.AddCommand(configEnvsAddCmd) + configEnvsAddCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)") + configEnvsCmd.AddCommand(configEnvsRemoveCmd) + configEnvsRemoveCmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)") +} + +var configEnvsCmd = &cobra.Command{ + Use: "envs", + Short: "List and manage configured environment variable for a function", + Long: `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. +`, + SuggestFor: []string{"ensv", "env"}, + PreRunE: bindEnv("path"), + RunE: func(cmd *cobra.Command, args []string) (err error) { + function, err := initConfigCommand(args) + if err != nil { + return + } + + listEnvs(function) + + return + }, +} + +var configEnvsAddCmd = &cobra.Command{ + Use: "add", + Short: "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. + +The environment variable can be set directly from a value, +from an environment variable on the local machine or from Secrets and ConfigMaps. +`, + SuggestFor: []string{"ad", "create", "insert", "append"}, + PreRunE: bindEnv("path"), + RunE: func(cmd *cobra.Command, args []string) (err error) { + function, err := initConfigCommand(args) + if err != nil { + return + } + + return runAddEnvsPrompt(cmd.Context(), function) + }, +} + +var configEnvsRemoveCmd = &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. +`, + SuggestFor: []string{"del", "delete", "rmeove"}, + PreRunE: bindEnv("path"), + RunE: func(cmd *cobra.Command, args []string) (err error) { + function, err := initConfigCommand(args) + if err != nil { + return + } + + return runRemoveEnvsPrompt(function) + }, +} + +func listEnvs(f bosonFunc.Function) { + if len(f.Envs) == 0 { + fmt.Println("There aren't any configured Environment variables") + return + } + + fmt.Println("Configured Environment variables:") + for _, e := range f.Envs { + fmt.Println(" - ", e.String()) + } +} + +func runAddEnvsPrompt(ctx context.Context, f bosonFunc.Function) (err error) { + + insertToIndex := 0 + + // SECTION - if there are some envs already set, let choose the position of the new entry + if len(f.Envs) > 0 { + options := []string{} + for _, e := range f.Envs { + options = append(options, fmt.Sprintf("Insert before: %s", e.String())) + } + options = append(options, "Insert here.") + + selectedEnv := "" + prompt := &survey.Select{ + Message: "Where do you want to add the Environment variable?", + Options: options, + Default: options[len(options)-1], + } + err = survey.AskOne(prompt, &selectedEnv) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + for i, option := range options { + if option == selectedEnv { + insertToIndex = i + break + } + } + } + + // SECTION - select the type of Environment variable to be added + secrets, err := k8s.ListSecretsNames(ctx, f.Namespace) + if err != nil { + return + } + configMaps, err := k8s.ListConfigMapsNames(ctx, f.Namespace) + if err != nil { + return + } + + selectedOption := "" + const ( + optionEnvValue = "Environment variable with a specified value" + optionEnvLocal = "Value from a local environment variable" + optionEnvConfigMap = "ConfigMap: all key=value pairs as environment variables" + optionEnvConfigMapKey = "ConfigMap: value from a key" + optionEnvSecret = "Secret: all key=value pairs as environment variables" + optionEnvSecretKey = "Secret: value from a key" + ) + options := []string{optionEnvValue, optionEnvLocal} + + if len(configMaps) > 0 { + options = append(options, optionEnvConfigMap) + options = append(options, optionEnvConfigMapKey) + } + if len(secrets) > 0 { + options = append(options, optionEnvSecret) + options = append(options, optionEnvSecretKey) + } + + err = survey.AskOne(&survey.Select{ + Message: "What type of Environment variable do you want to add?", + Options: options, + }, &selectedOption) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + newEnv := bosonFunc.Env{} + + switch selectedOption { + // SECTION - add new Environment variable with the specified value + case optionEnvValue: + qs := []*survey.Question{ + { + Name: "name", + Prompt: &survey.Input{Message: "Please specify the Environment variable name:"}, + Validate: func(val interface{}) error { + return utils.ValidateEnvVarName(val.(string)) + }, + }, + { + Name: "value", + Prompt: &survey.Input{Message: "Please specify the Environment variable value:"}, + }, + } + answers := struct { + Name string + Value string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + newEnv.Name = &answers.Name + newEnv.Value = &answers.Value + + // SECTION - add new Environment variable with value from a local environment variable + case optionEnvLocal: + qs := []*survey.Question{ + { + Name: "name", + Prompt: &survey.Input{Message: "Please specify the Environment variable name:"}, + Validate: func(val interface{}) error { + return utils.ValidateEnvVarName(val.(string)) + }, + }, + { + Name: "value", + Prompt: &survey.Input{Message: "Please specify the local Environment variable:"}, + Validate: survey.Required, + }, + } + answers := struct { + Name string + Value string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + if err == terminal.InterruptErr { + return 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) + newEnv.Name = &answers.Name + newEnv.Value = &value + + // SECTION - Add all key=value pairs from ConfigMap as environment variables + case optionEnvConfigMap: + selectedResource := "" + err = survey.AskOne(&survey.Select{ + Message: "Which ConfigMap do you want to use?", + Options: configMaps, + }, &selectedResource) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + value := fmt.Sprintf("{{ configMap:%s }}", selectedResource) + newEnv.Value = &value + + // SECTION - Environment variable with value from a key from ConfigMap + case optionEnvConfigMapKey: + qs := []*survey.Question{ + { + Name: "configmap", + Prompt: &survey.Select{ + Message: "Which ConfigMap do you want to use?", + Options: configMaps, + }, + }, + { + Name: "name", + Prompt: &survey.Input{Message: "Please specify the Environment variable name:"}, + Validate: func(val interface{}) error { + return utils.ValidateEnvVarName(val.(string)) + }, + }, + { + Name: "key", + Prompt: &survey.Input{Message: "Please specify a key from the selected ConfigMap:"}, + Validate: survey.Required, + }, + } + answers := struct { + ConfigMap string + Name string + Key string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + value := fmt.Sprintf("{{ configMap:%s:%s }}", answers.ConfigMap, answers.Key) + newEnv.Name = &answers.Name + newEnv.Value = &value + + // SECTION - Add all key=value pairs from Secret as environment variables + case optionEnvSecret: + selectedResource := "" + err = survey.AskOne(&survey.Select{ + Message: "Which Secret do you want to use?", + Options: secrets, + }, &selectedResource) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + value := fmt.Sprintf("{{ secret:%s }}", selectedResource) + newEnv.Value = &value + + // SECTION - Environment variable with value from a key from Secret + case optionEnvSecretKey: + qs := []*survey.Question{ + { + Name: "secret", + Prompt: &survey.Select{ + Message: "Which Secret do you want to use?", + Options: secrets, + }, + }, + { + Name: "name", + Prompt: &survey.Input{Message: "Please specify the Environment variable name:"}, + Validate: func(val interface{}) error { + return utils.ValidateEnvVarName(val.(string)) + }, + }, + { + Name: "key", + Prompt: &survey.Input{Message: "Please specify a key from the selected Secret:"}, + Validate: survey.Required, + }, + } + answers := struct { + Secret string + Name string + Key string + }{} + + err = survey.Ask(qs, &answers) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + value := fmt.Sprintf("{{ secret:%s:%s }}", answers.Secret, answers.Key) + newEnv.Name = &answers.Name + newEnv.Value = &value + } + + // we have all necessary information -> let's insert the env to the selected position in the list + if insertToIndex == len(f.Envs) { + f.Envs = append(f.Envs, newEnv) + } else { + f.Envs = append(f.Envs[:insertToIndex+1], f.Envs[insertToIndex:]...) + f.Envs[insertToIndex] = newEnv + } + + err = f.WriteConfig() + if err == nil { + fmt.Println("Environment variable entry was added to the function configuration") + } + + return +} + +func runRemoveEnvsPrompt(f bosonFunc.Function) (err error) { + if len(f.Envs) == 0 { + fmt.Println("There aren't any configured Environment variables") + return + } + + options := []string{} + for _, e := range f.Envs { + options = append(options, e.String()) + } + + selectedEnv := "" + prompt := &survey.Select{ + Message: "Which Environment variables do you want to remove?", + Options: options, + } + err = survey.AskOne(prompt, &selectedEnv) + if err != nil { + if err == terminal.InterruptErr { + return nil + } + return + } + + var newEnvs bosonFunc.Envs + removed := false + for i, e := range f.Envs { + if e.String() == selectedEnv { + newEnvs = append(f.Envs[:i], f.Envs[i+1:]...) + removed = true + break + } + } + + if removed { + f.Envs = newEnvs + err = f.WriteConfig() + if err == nil { + fmt.Println("Environment variable entry was removed from the function configuration") + } + } + + return +} diff --git a/cmd/config_volumes.go b/cmd/config_volumes.go index 4655afd59..e5d08ade8 100644 --- a/cmd/config_volumes.go +++ b/cmd/config_volumes.go @@ -53,8 +53,7 @@ Interactive prompt to add Secrets and ConfigMaps as Volume mounts to the functio in the current directory or from the directory specified with --path. `, SuggestFor: []string{"ad", "create", "insert", "append"}, - //ValidArgsFunction: CompleteFunctionList, - PreRunE: bindEnv("path"), + PreRunE: bindEnv("path"), RunE: func(cmd *cobra.Command, args []string) (err error) { function, err := initConfigCommand(args) if err != nil { @@ -192,10 +191,10 @@ func runAddVolumesPrompt(ctx context.Context, f bosonFunc.Function) (err error) } f.Volumes = append(f.Volumes, newVolume) - + err = f.WriteConfig() if err == nil { - fmt.Println("Volume was added to the function configuration") + fmt.Println("Volume entry was added to the function configuration") } return @@ -239,7 +238,7 @@ func runRemoveVolumesPrompt(f bosonFunc.Function) (err error) { f.Volumes = newVolumes err = f.WriteConfig() if err == nil { - fmt.Println("Volume was removed from the function configuration") + fmt.Println("Volume entry was removed from the function configuration") } } diff --git a/config.go b/config.go index bd2f5abf1..70cc02401 100644 --- a/config.go +++ b/config.go @@ -47,6 +47,35 @@ type Env struct { Value *string `yaml:"value"` } +func (e Env) String() string { + if e.Name == nil && e.Value != nil { + match := regWholeSecret.FindStringSubmatch(*e.Value) + if len(match) == 2 { + return fmt.Sprintf("All key=value pairs from Secret \"%s\"", match[1]) + } + match = regWholeConfigMap.FindStringSubmatch(*e.Value) + if len(match) == 2 { + return fmt.Sprintf("All key=value pairs from ConfigMap \"%s\"", match[1]) + } + } else if e.Name != nil && e.Value != nil { + match := regKeyFromSecret.FindStringSubmatch(*e.Value) + if len(match) == 3 { + return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from Secret \"%s\"", *e.Name, match[2], match[1]) + } + match = regKeyFromConfigMap.FindStringSubmatch(*e.Value) + if len(match) == 3 { + return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from ConfigMap \"%s\"", *e.Name, match[2], match[1]) + } + match = regLocalEnv.FindStringSubmatch(*e.Value) + if len(match) == 2 { + return fmt.Sprintf("Env \"%s\" with value set from local env variable \"%s\"", *e.Name, match[1]) + } + + return fmt.Sprintf("Env \"%s\" with value \"%s\"", *e.Name, *e.Value) + } + return "" +} + type Options struct { Scale *ScaleOptions `yaml:"scale,omitempty"` } diff --git a/docs/guides/commands.md b/docs/guides/commands.md index 40e9a15d3..5bcfd1f6c 100644 --- a/docs/guides/commands.md +++ b/docs/guides/commands.md @@ -185,17 +185,36 @@ Configured Volumes mounts: - ConfigMap "mycm" mounted at path: "/workspace/configmap" ``` +### `config envs` + +This command lists configured Environment variables: +```console +func config envs [-p ] +``` + +Invokes interactive prompt to add Environment variables to the function configuration +```console +func config envs add [-p ] +``` + +Invokes interactive prompt to remove Environment variables from the function configuration +```console +func config envs remove [-p ] +``` + +### `config volumes` + This command lists configured Volumes: ```console func config volumes [-p ] ``` -Invokes interactive prompt that allows addind Volumes to the function configuration +Invokes interactive prompt to add Volumes to the function configuration ```console func config volumes add [-p ] ``` -Invokes interactive prompt that allows removing Volumes from the function configuration +Invokes interactive prompt to remove Volumes from the function configuration ```console func config volumes remove [-p ] -``` \ No newline at end of file +```