diff --git a/README.md b/README.md index 7beb881..8c14ec4 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,17 @@ Usage: lw [command] Available Commands: - asset All things assets - auth authentication actions - cloud Interact with LiquidWeb's Cloud platform - completion Generate completion script - dedicated All things dedicated server - help Help about any command - network network actions - plan Process YAML plan file - ssh SSH to a Server - version show build information + asset All things assets + auth authentication actions + cloud Interact with LiquidWeb's Cloud platform + completion Generate completion script + dedicated All things dedicated server + default-flags Manage default flags + help Help about any command + network network actions + plan Process YAML plan file + ssh SSH to a Server + version show build information Flags: --config string config file (default is $HOME/.liquidweb-cli.yaml) diff --git a/cmd/cloudNetworkVipCreate.go b/cmd/cloudNetworkVipCreate.go index 1daa41b..3803ba2 100644 --- a/cmd/cloudNetworkVipCreate.go +++ b/cmd/cloudNetworkVipCreate.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/types/api" @@ -93,10 +94,6 @@ func init() { cloudNetworkVipCmd.AddCommand(cloudNetworkVipCreateCmd) cloudNetworkVipCreateCmd.Flags().String("name", fmt.Sprintf("vip-%s", utils.RandomString(8)), "name for the new VIP") - cloudNetworkVipCreateCmd.Flags().Int64("zone", -1, + cloudNetworkVipCreateCmd.Flags().Int64("zone", cast.ToInt64(defaultFlag("cloud_network_vip_create_zone", -1)), "zone id to create VIP in (see: 'cloud server options --zones')") - - if err := cloudNetworkVipCreateCmd.MarkFlagRequired("zone"); err != nil { - lwCliInst.Die(err) - } } diff --git a/cmd/cloudPrivateParentCreate.go b/cmd/cloudPrivateParentCreate.go index 7afd519..1b5a525 100644 --- a/cmd/cloudPrivateParentCreate.go +++ b/cmd/cloudPrivateParentCreate.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/types/api" @@ -69,17 +70,15 @@ of configs, check 'cloud server options --configs'.`, func init() { cloudPrivateParentCmd.AddCommand(cloudPrivateParentCreateCmd) - cloudPrivateParentCreateCmd.Flags().Int64("config-id", -1, "config-id (category must be bare-metal or bare-metal-r)") + cloudPrivateParentCreateCmd.Flags().Int64("config-id", cast.ToInt64(defaultFlag("cloud_private-parent_create_config-id", -1)), "config-id (category must be bare-metal or bare-metal-r)") cloudPrivateParentCreateCmd.Flags().String("name", "", "name for your Private Parent") - cloudPrivateParentCreateCmd.Flags().Int64("zone", -1, "id number of the zone to provision the Private Parent in ('cloud server options --zones')") + cloudPrivateParentCreateCmd.Flags().Int64("zone", cast.ToInt64(defaultFlag("cloud_private-parent_create_zone", -1)), + "id number of the zone to provision the Private Parent in ('cloud server options --zones')") - if err := cloudPrivateParentCreateCmd.MarkFlagRequired("config-id"); err != nil { - lwCliInst.Die(err) - } - if err := cloudPrivateParentCreateCmd.MarkFlagRequired("zone"); err != nil { - lwCliInst.Die(err) - } - if err := cloudPrivateParentCreateCmd.MarkFlagRequired("name"); err != nil { - lwCliInst.Die(err) + reqs := []string{"name"} + for _, req := range reqs { + if err := cloudPrivateParentCreateCmd.MarkFlagRequired(req); err != nil { + lwCliInst.Die(err) + } } } diff --git a/cmd/cloudServerClone.go b/cmd/cloudServerClone.go index fddf100..211f472 100644 --- a/cmd/cloudServerClone.go +++ b/cmd/cloudServerClone.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/types/api" @@ -64,9 +65,6 @@ Server is not on a Private Parent.`, hostnameFlag: map[string]string{"type": "NonEmptyString", "optional": "false"}, } - if privateParentFlag != "" && configIdFlag != -1 { - lwCliInst.Die(fmt.Errorf("cant pass both --config-id and --private-parent flags")) - } if privateParentFlag == "" && configIdFlag == -1 { lwCliInst.Die(fmt.Errorf("must pass --config-id or --private-parent")) } @@ -114,7 +112,7 @@ Server is not on a Private Parent.`, cloneArgs["vcpu"] = vcpuFlag validateFields[vcpuFlag] = "PositiveInt64" } - if configIdFlag != -1 { + if configIdFlag != -1 && privateParentFlag == "" { cloneArgs["config_id"] = configIdFlag validateFields[configIdFlag] = "PositiveInt64" } @@ -162,7 +160,7 @@ func init() { cloudServerCloneCmd.Flags().Int64("vcpu", -1, "amount of vcpus for new Cloud Server (when private-parent)") // Non Private Parent - cloudServerCloneCmd.Flags().Int64("config-id", -1, + cloudServerCloneCmd.Flags().Int64("config-id", cast.ToInt64(defaultFlag("cloud_server_clone_config-id", -1)), "config-id for new Cloud Server (when !private-parent) (see: 'cloud server options --configs')") if err := cloudServerCloneCmd.MarkFlagRequired("uniq-id"); err != nil { diff --git a/cmd/cloudServerCreate.go b/cmd/cloudServerCreate.go index de32890..e8fc967 100644 --- a/cmd/cloudServerCreate.go +++ b/cmd/cloudServerCreate.go @@ -134,17 +134,17 @@ func init() { sshPubKeyFile = fmt.Sprintf("%s/.ssh/id_rsa.pub", home) } - cloudServerCreateCmd.Flags().String("template", "", "template to use (see 'cloud server options --templates')") + cloudServerCreateCmd.Flags().String("template", cast.ToString(defaultFlag("cloud_server_create_template")), "template to use (see 'cloud server options --templates')") cloudServerCreateCmd.Flags().String("type", "SS.VPS", "some examples of types; SS.VPS, SS.VPS.WIN, SS.VM, SS.VM.WIN") cloudServerCreateCmd.Flags().String("hostname", "", "hostname to set") cloudServerCreateCmd.Flags().Int("ips", 1, "amount of IP addresses") cloudServerCreateCmd.Flags().String("public-ssh-key", sshPubKeyFile, "path to file containing the public ssh key you wish to be on the new Cloud Server") - cloudServerCreateCmd.Flags().Int("config-id", 0, "config-id to use") + cloudServerCreateCmd.Flags().Int("config-id", cast.ToInt(defaultFlag("cloud_server_create_config-id", -1)), "config-id to use") cloudServerCreateCmd.Flags().Int("backup-days", -1, "Enable daily backup plan. This is the amount of days to keep a backup") cloudServerCreateCmd.Flags().Int("backup-quota", -1, "Enable quota backup plan. This is the total amount of GB to keep.") cloudServerCreateCmd.Flags().String("bandwidth", "SS.10000", "bandwidth package to use") - cloudServerCreateCmd.Flags().Int64("zone", 0, "zone (id) to create new Cloud Server in (see 'cloud server options --zones')") + cloudServerCreateCmd.Flags().Int64("zone", cast.ToInt64(defaultFlag("cloud_server_create_zone", -1)), "zone (id) to create new Cloud Server in (see 'cloud server options --zones')") cloudServerCreateCmd.Flags().String("password", "", "root or administrator password to set") cloudServerCreateCmd.Flags().Int("backup-id", -1, "id of cloud backup to create from (see 'cloud backup list')") diff --git a/cmd/cloudServerResize.go b/cmd/cloudServerResize.go index c0dc709..ea424e7 100644 --- a/cmd/cloudServerResize.go +++ b/cmd/cloudServerResize.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/instance" @@ -104,7 +105,7 @@ func init() { cloudServerResizeCmd.Flags().Int64("memory", -1, "desired memory (when private-parent)") cloudServerResizeCmd.Flags().Bool("skip-fs-resize", false, "whether or not to skip the fs resize") cloudServerResizeCmd.Flags().Int64("vcpu", -1, "desired vcpu count (when private-parent)") - cloudServerResizeCmd.Flags().Int64("config-id", -1, + cloudServerResizeCmd.Flags().Int64("config-id", cast.ToInt64(defaultFlag("cloud_server_resize_config-id", -1)), "config-id of your desired config (when !private-parent) (see 'cloud server options --configs')") if err := cloudServerResizeCmd.MarkFlagRequired("uniq-id"); err != nil { diff --git a/cmd/cloudTemplateRestore.go b/cmd/cloudTemplateRestore.go index d3e2b1c..abad64c 100644 --- a/cmd/cloudTemplateRestore.go +++ b/cmd/cloudTemplateRestore.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/instance" @@ -59,12 +60,9 @@ func init() { cloudTemplateCmd.AddCommand(cloudTemplateRestoreCmd) cloudTemplateRestoreCmd.Flags().String("uniq-id", "", "uniq-id of Cloud Server") - cloudTemplateRestoreCmd.Flags().String("template", "", "name of template to restore") + cloudTemplateRestoreCmd.Flags().String("template", cast.ToString(defaultFlag("cloud_template_restore_template")), "name of template to restore") if err := cloudTemplateRestoreCmd.MarkFlagRequired("uniq-id"); err != nil { lwCliInst.Die(err) } - if err := cloudTemplateRestoreCmd.MarkFlagRequired("template"); err != nil { - lwCliInst.Die(err) - } } diff --git a/cmd/defaultFlags.go b/cmd/defaultFlags.go new file mode 100644 index 0000000..f83e19e --- /dev/null +++ b/cmd/defaultFlags.go @@ -0,0 +1,44 @@ +/* +Copyright © LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var defaultFlagsCmd = &cobra.Command{ + Use: "default-flags", + Short: "Manage default flags", + Long: `Manage the configured default flags. + +If a default flag is set (such as for "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'. + +For a full list of capabilities, please refer to the "Available Commands" section.`, + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + lwCliInst.Die(err) + } + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(defaultFlagsCmd) +} diff --git a/cmd/defaultFlagsDelete.go b/cmd/defaultFlagsDelete.go new file mode 100644 index 0000000..a8abc65 --- /dev/null +++ b/cmd/defaultFlagsDelete.go @@ -0,0 +1,51 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsDeleteCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a flag", + Long: `Delete a flag for the current context. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + flagName, _ := cmd.Flags().GetString("flag") + + if err := defaults.Delete(flagName); err != nil { + lwCliInst.Die(err) + } + + fmt.Printf("deleted default flag [%s]\n", flagName) + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsDeleteCmd) + defaultFlagsDeleteCmd.Flags().String("flag", "", "name of the flag to delete") + if err := defaultFlagsDeleteCmd.MarkFlagRequired("flag"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/cmd/defaultFlagsGet.go b/cmd/defaultFlagsGet.go new file mode 100644 index 0000000..5636101 --- /dev/null +++ b/cmd/defaultFlagsGet.go @@ -0,0 +1,53 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get details on a flag", + Long: `Get details on a flag in the current context. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + flagName, _ := cmd.Flags().GetString("flag") + + value, err := defaults.Get(flagName) + if err != nil { + lwCliInst.Die(err) + } + + fmt.Printf("flag: %s\n", flagName) + fmt.Printf("\tvalue: %+v\n", value) + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsGetCmd) + defaultFlagsGetCmd.Flags().String("flag", "", "name of the flag") + if err := defaultFlagsGetCmd.MarkFlagRequired("flag"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/cmd/defaultFlagsList.go b/cmd/defaultFlagsList.go new file mode 100644 index 0000000..587b2e0 --- /dev/null +++ b/cmd/defaultFlagsList.go @@ -0,0 +1,47 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsListCmd = &cobra.Command{ + Use: "list", + Short: "Display your set default flags", + Long: `Display your set default flags. + +If you've never created any default flags, see 'help default-flags set'. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + all, err := defaults.GetAll() + if err != nil { + lwCliInst.Die(err) + } + fmt.Print(all) + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsListCmd) +} diff --git a/cmd/defaultFlagsNagOff.go b/cmd/defaultFlagsNagOff.go new file mode 100644 index 0000000..2e5c25a --- /dev/null +++ b/cmd/defaultFlagsNagOff.go @@ -0,0 +1,45 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsNagOffCmd = &cobra.Command{ + Use: "nags-off", + Short: "Turn nags off", + Long: `Turn nags off for unset default flags. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + if err := defaults.NagsOff(); err != nil { + lwCliInst.Die(err) + } + + fmt.Println("Nags turned off.") + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsNagOffCmd) +} diff --git a/cmd/defaultFlagsNagOn.go b/cmd/defaultFlagsNagOn.go new file mode 100644 index 0000000..2ff0522 --- /dev/null +++ b/cmd/defaultFlagsNagOn.go @@ -0,0 +1,45 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsNagOnCmd = &cobra.Command{ + Use: "nags-on", + Short: "Turn nags on", + Long: `Turn nags on for unset default flags. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + if err := defaults.NagsOn(); err != nil { + lwCliInst.Die(err) + } + + fmt.Println("Nags turned on.") + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsNagOnCmd) +} diff --git a/cmd/defaultFlagsPermitted.go b/cmd/defaultFlagsPermitted.go new file mode 100644 index 0000000..74eceb3 --- /dev/null +++ b/cmd/defaultFlagsPermitted.go @@ -0,0 +1,47 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsPermittedCmd = &cobra.Command{ + Use: "permitted", + Short: "Display permitted default flags", + Long: `Display permitted default flags. + +If you've never created any default flags, see 'help default-flags set'. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + permitted := defaults.GetPermitted() + fmt.Println("Permitted flags:") + for _, flag := range permitted { + fmt.Printf(" %s\n", flag) + } + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsPermittedCmd) +} diff --git a/cmd/defaultFlagsSet.go b/cmd/defaultFlagsSet.go new file mode 100644 index 0000000..48949a6 --- /dev/null +++ b/cmd/defaultFlagsSet.go @@ -0,0 +1,56 @@ +/* +Copyright © 2019 LiquidWeb + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/flags/defaults" +) + +var defaultFlagsSetCmd = &cobra.Command{ + Use: "set", + Short: "Set a flag", + Long: `Set a flag for the current context. + +When a default flag is set (such as "zone") then any subcommand will use its +value in place if omitted. Default flags are auth context aware. For details +on auth contexts, see 'help auth'.`, + Run: func(cmd *cobra.Command, args []string) { + flagName, _ := cmd.Flags().GetString("flag") + flagValue, _ := cmd.Flags().GetString("value") + + if err := defaults.Set(flagName, flagValue); err != nil { + lwCliInst.Die(err) + } + + fmt.Printf("flag [%s] set with value [%s]\n", flagName, flagValue) + }, +} + +func init() { + defaultFlagsCmd.AddCommand(defaultFlagsSetCmd) + defaultFlagsSetCmd.Flags().String("flag", "", "name of the flag to set") + defaultFlagsSetCmd.Flags().String("value", "", "value for the flag") + reqs := []string{"flag", "value"} + for _, req := range reqs { + if err := defaultFlagsSetCmd.MarkFlagRequired(req); err != nil { + lwCliInst.Die(err) + } + } +} diff --git a/cmd/networkIpPoolCreate.go b/cmd/networkIpPoolCreate.go index d31ae1f..4c54ce8 100644 --- a/cmd/networkIpPoolCreate.go +++ b/cmd/networkIpPoolCreate.go @@ -18,6 +18,7 @@ package cmd import ( "fmt" + "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/liquidweb/liquidweb-cli/types/api" @@ -36,11 +37,8 @@ your account.`, zoneFlag, _ := cmd.Flags().GetInt64("zone") newIpsFlag, _ := cmd.Flags().GetInt64("new-ips") - //fmt.Printf("networkIpPoolCreateCmdAddIpsFlag: %+v\n", networkIpPoolCreateCmdAddIpsFlag) - //fmt.Printf("%d %d\n", newIpsFlag, zoneFlag) - - if len(networkIpPoolCreateCmdAddIpsFlag) == 0 && newIpsFlag == -1 { - lwCliInst.Die(fmt.Errorf("flags --new-ips --add-ips cannot both be empty")) + if len(networkIpPoolCreateCmdAddIpsFlag) == 0 && newIpsFlag == -1 || zoneFlag == -1 { + lwCliInst.Die(fmt.Errorf("flags --new-ips --add-ips cannot both be empty. --zone cannot be empty")) } apiArgs := map[string]interface{}{ @@ -68,9 +66,6 @@ func init() { networkIpPoolCreateCmd.Flags().StringSliceVar(&networkIpPoolCreateCmdAddIpsFlag, "add-ips", []string{}, "ips separated by ',' to add to created IP Pool") networkIpPoolCreateCmd.Flags().Int64("new-ips", -1, "amount of IPs to assign to the created IP Pool") - networkIpPoolCreateCmd.Flags().Int64("zone", -1, "zone id to create the IP Pool in") - - if err := networkIpPoolCreateCmd.MarkFlagRequired("zone"); err != nil { - lwCliInst.Die(err) - } + networkIpPoolCreateCmd.Flags().Int64("zone", cast.ToInt64(defaultFlag("network_ip-pool_create_zone", -1)), + "zone id to create the IP Pool in") } diff --git a/cmd/root.go b/cmd/root.go index b7c3766..d69b5c7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,19 +18,20 @@ package cmd import ( "fmt" "os" + "regexp" "strings" "github.com/c-bata/go-prompt" - homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/spf13/viper" + "github.com/liquidweb/liquidweb-cli/config" + "github.com/liquidweb/liquidweb-cli/flags/defaults" "github.com/liquidweb/liquidweb-cli/instance" "github.com/liquidweb/liquidweb-cli/utils" ) var cfgFile string -var lwCliInst instance.Client +var lwCliInst *instance.Client var useContext string var rootCmd = &cobra.Command{ @@ -68,35 +69,53 @@ func init() { rootCmd.PersistentFlags().StringVar(&useContext, "use-context", "", "forces current context, without persisting the context change") } -func initConfig() { - vp := viper.New() - if cfgFile != "" { - // Use config file from the flag. - vp.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - lwCliInst.Die(err) +func setConfigArgs() { + config.UseContextArg = useContext + config.ConfigFileArg = cfgFile + + if config.UseContextArg == "" { + osArgsForContext(regexp.MustCompile(`use-context\s+[A-z]+`)) + if config.UseContextArg == "" { + osArgsForContext(regexp.MustCompile(`use-context=[A-z]+`)) } + } +} - // Search config in home directory with name ".liquidweb-cli" (without extension). - vp.AddConfigPath(home) - vp.SetConfigName(".liquidweb-cli") +// this gets called early on before cobra fully initializes, so have to +// parse os.Args directly. +func osArgsForContext(re *regexp.Regexp) { + var searchStr string + for _, str := range os.Args { + searchStr = searchStr + " " + str } - vp.AutomaticEnv() - if err := vp.ReadInConfig(); err != nil { - utils.PrintYellow("no config\n") + delimiter := " " + if strings.Contains(searchStr, "=") { + delimiter = "=" } - if useContext != "" { - if err := instance.ValidateContext(useContext, vp); err != nil { - utils.PrintRed("error using auth context:\n\n") - fmt.Printf("%s\n\n", err) - os.Exit(1) - } - vp.Set("liquidweb.api.current_context", useContext) + slice := strings.Split(re.FindString(searchStr), delimiter) + if len(slice) > 1 && slice[1] != "" { + config.UseContextArg = slice[1] + config.CurrentContext = slice[1] + } +} + +func defaultFlag(flag string, defaultValueList ...interface{}) (value interface{}) { + // calling config.InitConfig() here so default context gets set + _, _ = config.InitConfig() + setConfigArgs() + value = defaults.GetOrNag(flag) + if len(defaultValueList) > 0 && value == nil { + value = defaultValueList[0] + } + return +} + +func initConfig() { + vp, err := config.InitConfig() + if err != nil { + lwCliInst.Die(err) } var lwCliInstErr error diff --git a/config/core.go b/config/core.go new file mode 100644 index 0000000..bcf1319 --- /dev/null +++ b/config/core.go @@ -0,0 +1,57 @@ +package config + +import ( + "fmt" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" + + "github.com/liquidweb/liquidweb-cli/instance" + "github.com/liquidweb/liquidweb-cli/utils" +) + +var ( + CurrentContext string + ConfigFileArg string + UseContextArg string +) + +func InitConfig() (vp *viper.Viper, err error) { + vp = viper.New() + + if ConfigFileArg != "" { + // Use config file from the flag. + vp.SetConfigFile(ConfigFileArg) + } else { + // Search config in home directory with name ".liquidweb-cli" (without extension). + var home string + home, err = homedir.Dir() + if err != nil { + return + } + vp.AddConfigPath(home) + vp.SetConfigName(".liquidweb-cli") + } + + vp.AutomaticEnv() + if err = vp.ReadInConfig(); err != nil { + if _, notFound := err.(viper.ConfigFileNotFoundError); notFound { + err = nil + return + } + utils.PrintYellow("error reading config: %s\n", err) + return + } + + if UseContextArg != "" { + if err = instance.ValidateContext(UseContextArg, vp); err != nil { + err = fmt.Errorf("error using auth context: %s\n", err) + return + } + vp.Set("liquidweb.api.current_context", UseContextArg) + } + + CurrentContext = vp.GetString("liquidweb.api.current_context") + + return +} diff --git a/flags/defaults/constants.go b/flags/defaults/constants.go new file mode 100644 index 0000000..371093e --- /dev/null +++ b/flags/defaults/constants.go @@ -0,0 +1,5 @@ +package defaults + +const NagsKey = "nags" +const DefFlagsKey = "defaults" +const DefaultFlagsFileKey = "liquidweb.flags.defaults.file" diff --git a/flags/defaults/core.go b/flags/defaults/core.go new file mode 100644 index 0000000..a7d88f4 --- /dev/null +++ b/flags/defaults/core.go @@ -0,0 +1,291 @@ +package defaults + +import ( + "errors" + "fmt" + "os" + "sort" + "strings" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/cast" + "github.com/spf13/viper" + + "github.com/liquidweb/liquidweb-cli/config" + "github.com/liquidweb/liquidweb-cli/utils" + "github.com/liquidweb/liquidweb-cli/validate" +) + +var ( + nagged map[string]bool + nags bool + tipped bool +) + +func init() { + nagged = map[string]bool{} + + home, err := homedir.Dir() + if err != nil { + utils.PrintYellow("failed fetching homedir: %s\n", err) + return + } + viper.SetDefault("liquidweb.flags.defaults.file", fmt.Sprintf("%s/.liquidweb-cli-flag-defaults.yaml", home)) +} + +func NagsOff() (err error) { + err = toggleNags(false) + + return +} + +func NagsOn() (err error) { + err = toggleNags(true) + + return +} + +func GetPermitted() (permitted []string) { + permitted = make([]string, 0, len(permittedFlags)) + for flag, opts := range permittedFlags { + if enabled, ok := opts["enabled"].(bool); !enabled || !ok { + continue + } + permitted = append(permitted, flag) + } + sort.Strings(permitted) + + return +} + +func GetOrNag(flag string) (value interface{}) { + var err error + value, err = Get(flag) + if err != nil { + if !nagged[flag] { + if errors.Is(err, ErrorNotFound) { + if nags { + fmt.Printf("No default value for flag [%s] set. See 'help default-flags set' for details.\n", flag) + if !tipped { + utils.PrintTeal("TIP: You can silence undefined default flag notices with 'default-flags nags-off'\n") + tipped = true + } + } + } else { + utils.PrintYellow("WARNING: Unexpected error when fetching value for default flag [%s]: %s\n", flag, err) + } + nagged[flag] = true + } + } + return +} + +func Get(flag string) (value interface{}, err error) { + if _, err = permittedFlagOrError(flag); err != nil { + return + } + + var flags map[string]interface{} + flags, err = getFlagsMap() + if err != nil { + return + } + + if v, exists := flags[flag]; exists { + value = v + return + } + + err = fmt.Errorf("%s %w", flag, ErrorNotFound) + return +} + +func GetAll() (all AllFlags, err error) { + all, err = getFlagsMap() + + return +} + +func Set(flag string, value interface{}) (err error) { + var validator string + validator, err = permittedFlagOrError(flag) + if err != nil { + return + } + + if err = validateFlagValue(validator, value); err != nil { + return + } + + var ( + vp *viper.Viper + flags map[string]interface{} + ) + vp, flags, err = getFlagsViperAndMap() + if err != nil { + return + } + + flags[flag] = value + vp.Set(contextFlagKey(), flags) + + err = writeViperConfig(vp) + + return +} + +func Delete(flag string) (err error) { + if _, err = permittedFlagOrError(flag); err != nil { + return + } + + var ( + vp *viper.Viper + flags map[string]interface{} + ) + vp, flags, err = getFlagsViperAndMap() + if err != nil { + return + } + + delete(flags, flag) + vp.Set(contextFlagKey(), flags) + err = writeViperConfig(vp) + + return +} + +func permittedFlagOrError(flag string) (validator string, err error) { + if flag == "" { + err = ErrorInvalidFlagName + return + } + + opts, exists := permittedFlags[flag] + if !exists { + err = fmt.Errorf("%s %w", flag, ErrorForbiddenFlag) + return + } + + if v, ok := opts["enabled"].(bool); !ok || !v { + err = fmt.Errorf("%s %w", flag, ErrorForbiddenFlag) + } + + validator = cast.ToString(opts["type"]) + + return +} + +func validateFlagValue(validator string, value interface{}) (err error) { + var validateFields map[interface{}]interface{} + + if strings.HasSuffix(validator, "Int64") { + validateFields = map[interface{}]interface{}{ + cast.ToInt64(value): "PositiveInt64", + } + } else if strings.HasSuffix(validator, "Int") { + validateFields = map[interface{}]interface{}{ + cast.ToInt(value): "PositiveInt64", + } + } else if strings.HasSuffix(validator, "String") { + validateFields = map[interface{}]interface{}{ + cast.ToString(value): "NonEmptyString", + } + } + + err = validate.Validate(validateFields) + + return +} + +func getFlagsViperAndMap() (vp *viper.Viper, flags map[string]interface{}, err error) { + vp, err = getFlagsViper() + if err != nil { + return + } + + flags, err = getFlagsMap(vp) + + return +} + +func getFlagsMap(vpL ...*viper.Viper) (flags map[string]interface{}, err error) { + var vp *viper.Viper + if len(vpL) == 0 { + if vp, err = getFlagsViper(); err != nil { + return + } + } else { + vp = vpL[0] + } + + flags = vp.GetStringMap(contextFlagKey()) + + return +} + +func getFlagsViper() (vp *viper.Viper, err error) { + var file string + file, err = getFlagsFile() + if err != nil { + return + } + + vp = viper.New() + vp.SetConfigFile(file) + vp.SetDefault(NagsKey, true) + if err = vp.ReadInConfig(); err != nil { + err = fmt.Errorf("%w: %s", ErrorUnreadable, err) + return + } + + nags = vp.GetBool(NagsKey) + + return +} + +func getFlagsFile() (file string, err error) { + file = viper.GetString(DefaultFlagsFileKey) + if file == "" { + err = ErrorFileKeyMissing + } + + if _, err = os.Stat(file); os.IsNotExist(err) { + err = nil + f, ferr := os.Create(file) + if ferr != nil { + err = ferr + return + } + err = f.Close() + } + + return +} + +func contextFlagKey() (k string) { + k = fmt.Sprintf("%s.%s", DefFlagsKey, config.CurrentContext) + + return +} + +func toggleNags(on bool) error { + vp, err := getFlagsViper() + if err != nil { + return err + } + + vp.Set(NagsKey, on) + + err = writeViperConfig(vp) + + return err +} + +func writeViperConfig(vp *viper.Viper) (err error) { + if err = vp.WriteConfig(); err != nil { + err = fmt.Errorf("%w: %s", ErrorUnwritable, err) + } + + return +} diff --git a/flags/defaults/errors.go b/flags/defaults/errors.go new file mode 100644 index 0000000..09bdc98 --- /dev/null +++ b/flags/defaults/errors.go @@ -0,0 +1,12 @@ +package defaults + +import ( + "errors" +) + +var ErrorForbiddenFlag = errors.New("is a forbidden default flag") +var ErrorInvalidFlagName = errors.New("the given flag name is invalid") +var ErrorFileKeyMissing = errors.New("flag defaults file key is missing") +var ErrorUnwritable = errors.New("flag defaults cannot be written") +var ErrorUnreadable = errors.New("flag defaults cannot be read") +var ErrorNotFound = errors.New("flag default not found") diff --git a/flags/defaults/types.go b/flags/defaults/types.go new file mode 100644 index 0000000..bdf8dd9 --- /dev/null +++ b/flags/defaults/types.go @@ -0,0 +1,75 @@ +package defaults + +import ( + "fmt" + "strings" +) + +type AllFlags map[string]interface{} + +func (self AllFlags) String() string { + var slice []string + + if len(self) == 0 { + slice = append(slice, "No configured default flags. Set some with 'default-flags set'.\n") + } else { + slice = append(slice, "Configured default flags:\n\n") + + for flag, value := range self { + slice = append(slice, fmt.Sprintf(" Flag: %s\n", flag)) + slice = append(slice, fmt.Sprintf(" Value: %+v\n", value)) + } + } + + return strings.Join(slice[:], "") +} + +var permittedFlags = map[string]map[string]interface{}{ + // cloud network vip create + "cloud_network_vip_create_zone": map[string]interface{}{ + "enabled": true, + "validator": "PositiveInt64", + }, + // cloud private-parent create + "cloud_private-parent_create_config-id": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + "cloud_private-parent_create_zone": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + // cloud server clone + "cloud_server_clone_config-id": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + // cloud server create + "cloud_server_create_zone": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + "cloud_server_create_template": map[string]interface{}{ + "enabled": true, + "type": "NonEmptyString", + }, + "cloud_server_create_config-id": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + // cloud server resize + "cloud_server_resize_config-id": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, + // cloud template restore + "cloud_template_restore_template": map[string]interface{}{ + "enabled": true, + "type": "NonEmptyString", + }, + // network ip-pool create + "network_ip-pool_create_zone": map[string]interface{}{ + "enabled": true, + "type": "PositiveInt64", + }, +} diff --git a/instance/cloudServerCreate.go b/instance/cloudServerCreate.go index ddfad04..779f629 100644 --- a/instance/cloudServerCreate.go +++ b/instance/cloudServerCreate.go @@ -98,9 +98,6 @@ func (ci *Client) CloudServerCreate(params *CloudServerCreateParams) (string, er // sanity check flags if params.PrivateParent != "" { - if params.ConfigId > 0 { - return "", fmt.Errorf("--config-id must be 0 or omitted when specifying --private-parent") - } // create on a private parent. diskspace, memory, vcpu are required. if params.Memory == -1 { return "", fmt.Errorf("--memory is required when specifying --private-parent") diff --git a/instance/cloudTemplateRestore.go b/instance/cloudTemplateRestore.go index 3ccb9e7..580f774 100644 --- a/instance/cloudTemplateRestore.go +++ b/instance/cloudTemplateRestore.go @@ -16,6 +16,8 @@ limitations under the License. package instance import ( + "errors" + "github.com/liquidweb/liquidweb-cli/types/api" "github.com/liquidweb/liquidweb-cli/validate" ) @@ -33,6 +35,10 @@ func (ci *Client) CloudTemplateRestore(params *CloudTemplateRestoreParams) (stri return "", err } + if params.Template == "" { + return "", errors.New("template cannot be blank") + } + apiArgs := map[string]interface{}{"template": params.Template, "uniq_id": params.UniqId} var details apiTypes.CloudTemplateRestoreResponse diff --git a/instance/instance.go b/instance/instance.go index 53bba7e..2114ba9 100644 --- a/instance/instance.go +++ b/instance/instance.go @@ -32,16 +32,15 @@ import ( "github.com/liquidweb/liquidweb-cli/utils" ) -func New(viper *viper.Viper) (Client, error) { - +func New(viper *viper.Viper) (*Client, error) { lwCliApiClient, err := lwCliInstApi.New(viper) if err != nil { - return Client{}, fmt.Errorf( + return &Client{}, fmt.Errorf( "Failed creating an lwApi client. Error was:\n%s\nPlease check your liquidweb-cli config file for errors or ommissions\n", err) } - client := Client{ + client := &Client{ LwCliApiClient: lwCliApiClient, Viper: viper, }