1/*
2Copyright 2018 The Doctl Authors All rights reserved.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6    http://www.apache.org/licenses/LICENSE-2.0
7Unless required by applicable law or agreed to in writing, software
8distributed under the License is distributed on an "AS IS" BASIS,
9WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10See the License for the specific language governing permissions and
11limitations under the License.
12*/
13
14package commands
15
16import (
17	"fmt"
18	"log"
19	"os"
20	"path/filepath"
21	"strings"
22
23	"github.com/digitalocean/doctl"
24	"github.com/fatih/color"
25	"github.com/spf13/cobra"
26	"github.com/spf13/viper"
27)
28
29const (
30	defaultConfigName = "config.yaml" // default name of config file
31)
32
33var (
34	//DoitCmd is the root level doctl command that all other commands attach to
35	DoitCmd = &Command{ // base command
36		Command: &cobra.Command{
37			Use:   "doctl",
38			Short: "doctl is a command line interface (CLI) for the DigitalOcean API.",
39		},
40	}
41
42	//Writer wires up stdout for all commands to write to
43	Writer = os.Stdout
44	//APIURL customize API base URL
45	APIURL string
46	//Context current auth context
47	Context string
48	//Output global output format
49	Output string
50	//Token global authorization token
51	Token string
52	//Trace toggles http tracing output
53	Trace bool
54	//Verbose toggle verbose output on and off
55	Verbose bool
56
57	requiredColor = color.New(color.Bold).SprintfFunc()
58)
59
60func init() {
61	var cfgFile string
62
63	initConfig()
64
65	rootPFlagSet := DoitCmd.PersistentFlags()
66	rootPFlagSet.StringVarP(&cfgFile, "config", "c",
67		filepath.Join(defaultConfigHome(), defaultConfigName), "Specify a custom config file")
68	viper.BindPFlag("config", rootPFlagSet.Lookup("config"))
69
70	rootPFlagSet.StringVarP(&APIURL, "api-url", "u", "", "Override default API endpoint")
71	viper.BindPFlag("api-url", rootPFlagSet.Lookup("api-url"))
72
73	rootPFlagSet.StringVarP(&Token, doctl.ArgAccessToken, "t", "", "API V2 access token")
74	viper.BindPFlag(doctl.ArgAccessToken, rootPFlagSet.Lookup(doctl.ArgAccessToken))
75
76	rootPFlagSet.StringVarP(&Output, doctl.ArgOutput, "o", "text", "Desired output format [text|json]")
77	viper.BindPFlag("output", rootPFlagSet.Lookup(doctl.ArgOutput))
78
79	rootPFlagSet.StringVarP(&Context, doctl.ArgContext, "", "", "Specify a custom authentication context name")
80	DoitCmd.RegisterFlagCompletionFunc(doctl.ArgContext, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
81		return getAuthContextList(), cobra.ShellCompDirectiveNoFileComp
82	})
83
84	rootPFlagSet.BoolVarP(&Trace, "trace", "", false, "Show a log of network activity while performing a command")
85	rootPFlagSet.BoolVarP(&Verbose, doctl.ArgVerbose, "v", false, "Enable verbose output")
86
87	addCommands()
88
89	cobra.OnInitialize(initConfig)
90}
91
92func initConfig() {
93	viper.SetEnvPrefix("DIGITALOCEAN")
94	viper.AutomaticEnv()
95	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
96	viper.SetConfigType("yaml")
97
98	cfgFile := viper.GetString("config")
99	viper.SetConfigFile(cfgFile)
100
101	viper.SetDefault("output", "text")
102	viper.SetDefault(doctl.ArgContext, doctl.ArgDefaultContext)
103
104	if _, err := os.Stat(cfgFile); err == nil {
105		if err := viper.ReadInConfig(); err != nil {
106			log.Fatalln("Config initialization failed:", err)
107		}
108	}
109}
110
111// in case we ever want to change this, or let folks configure it...
112func defaultConfigHome() string {
113	cfgDir, err := os.UserConfigDir()
114	checkErr(err)
115
116	return filepath.Join(cfgDir, "doctl")
117}
118
119func configHome() string {
120	ch := defaultConfigHome()
121	err := os.MkdirAll(ch, 0755)
122	checkErr(err)
123
124	return ch
125}
126
127// Execute executes the current command using DoitCmd.
128func Execute() {
129	if err := DoitCmd.Execute(); err != nil {
130		fmt.Println(err)
131		os.Exit(-1)
132	}
133}
134
135// AddCommands adds sub commands to the base command.
136func addCommands() {
137	DoitCmd.AddCommand(Account())
138	DoitCmd.AddCommand(Apps())
139	DoitCmd.AddCommand(Auth())
140	DoitCmd.AddCommand(Balance())
141	DoitCmd.AddCommand(BillingHistory())
142	DoitCmd.AddCommand(Invoices())
143	DoitCmd.AddCommand(computeCmd())
144	DoitCmd.AddCommand(Kubernetes())
145	DoitCmd.AddCommand(Databases())
146	DoitCmd.AddCommand(Projects())
147	DoitCmd.AddCommand(Version())
148	DoitCmd.AddCommand(Registry())
149	DoitCmd.AddCommand(VPCs())
150	DoitCmd.AddCommand(OneClicks())
151	DoitCmd.AddCommand(Monitoring())
152}
153
154func computeCmd() *Command {
155	cmd := &Command{
156		Command: &cobra.Command{
157			Use:   "compute",
158			Short: "Display commands that manage infrastructure",
159			Long:  `The subcommands under ` + "`" + `doctl compute` + "`" + ` are for managing DigitalOcean resources.`,
160		},
161	}
162
163	cmd.AddCommand(Actions())
164	cmd.AddCommand(CDN())
165	cmd.AddCommand(Certificate())
166	cmd.AddCommand(DropletAction())
167	cmd.AddCommand(Droplet())
168	cmd.AddCommand(Domain())
169	cmd.AddCommand(Firewall())
170	cmd.AddCommand(FloatingIP())
171	cmd.AddCommand(FloatingIPAction())
172	cmd.AddCommand(Images())
173	cmd.AddCommand(ImageAction())
174	cmd.AddCommand(LoadBalancer())
175	cmd.AddCommand(Plugin())
176	cmd.AddCommand(Region())
177	cmd.AddCommand(Size())
178	cmd.AddCommand(Snapshot())
179	cmd.AddCommand(SSHKeys())
180	cmd.AddCommand(Tags())
181	cmd.AddCommand(Volume())
182	cmd.AddCommand(VolumeAction())
183
184	// SSH is different since it doesn't have any subcommands. In this case, let's
185	// give it a parent at init time.
186	SSH(cmd)
187
188	return cmd
189}
190
191type flagOpt func(c *Command, name, key string)
192
193func requiredOpt() flagOpt {
194	return func(c *Command, name, key string) {
195		c.MarkFlagRequired(key)
196
197		key = fmt.Sprintf("required.%s", key)
198		viper.Set(key, true)
199
200		u := c.Flag(name).Usage
201		c.Flag(name).Usage = fmt.Sprintf("%s %s", u, requiredColor("(required)"))
202	}
203}
204
205// AddStringFlag adds a string flag to a command.
206func AddStringFlag(cmd *Command, name, shorthand, dflt, desc string, opts ...flagOpt) {
207	fn := flagName(cmd, name)
208	cmd.Flags().StringP(name, shorthand, dflt, desc)
209
210	for _, o := range opts {
211		o(cmd, name, fn)
212	}
213
214	viper.BindPFlag(fn, cmd.Flags().Lookup(name))
215}
216
217// AddIntFlag adds an integr flag to a command.
218func AddIntFlag(cmd *Command, name, shorthand string, def int, desc string, opts ...flagOpt) {
219	fn := flagName(cmd, name)
220	cmd.Flags().IntP(name, shorthand, def, desc)
221	viper.BindPFlag(fn, cmd.Flags().Lookup(name))
222
223	for _, o := range opts {
224		o(cmd, name, fn)
225	}
226}
227
228// AddBoolFlag adds a boolean flag to a command.
229func AddBoolFlag(cmd *Command, name, shorthand string, def bool, desc string, opts ...flagOpt) {
230	fn := flagName(cmd, name)
231	cmd.Flags().BoolP(name, shorthand, def, desc)
232	viper.BindPFlag(fn, cmd.Flags().Lookup(name))
233
234	for _, o := range opts {
235		o(cmd, name, fn)
236	}
237}
238
239// AddStringSliceFlag adds a string slice flag to a command.
240func AddStringSliceFlag(cmd *Command, name, shorthand string, def []string, desc string, opts ...flagOpt) {
241	fn := flagName(cmd, name)
242	cmd.Flags().StringSliceP(name, shorthand, def, desc)
243	viper.BindPFlag(fn, cmd.Flags().Lookup(name))
244
245	for _, o := range opts {
246		o(cmd, name, fn)
247	}
248}
249
250// AddStringMapStringFlag adds a map of strings by strings flag to a command.
251func AddStringMapStringFlag(cmd *Command, name, shorthand string, def map[string]string, desc string, opts ...flagOpt) {
252	fn := flagName(cmd, name)
253	cmd.Flags().StringToStringP(name, shorthand, def, desc)
254	viper.BindPFlag(fn, cmd.Flags().Lookup(name))
255
256	for _, o := range opts {
257		o(cmd, name, fn)
258	}
259}
260
261func flagName(cmd *Command, name string) string {
262	if cmd.Parent() != nil {
263		return fmt.Sprintf("%s.%s.%s", cmd.Parent().Name(), cmd.Name(), name)
264	}
265	return fmt.Sprintf("%s.%s", cmd.Name(), name)
266}
267
268func cmdNS(cmd *cobra.Command) string {
269	if cmd.Parent() != nil {
270		return fmt.Sprintf("%s.%s", cmd.Parent().Name(), cmd.Name())
271	}
272	return fmt.Sprintf("%s", cmd.Name())
273}
274