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 doctl
15
16import (
17	"bytes"
18	"context"
19	"encoding/json"
20	"errors"
21	"fmt"
22	"io"
23	"log"
24	"net/http"
25	"net/url"
26	"os"
27	"regexp"
28	"runtime"
29	"strconv"
30	"strings"
31
32	"github.com/blang/semver"
33	"github.com/digitalocean/doctl/pkg/listen"
34	"github.com/digitalocean/doctl/pkg/runner"
35	"github.com/digitalocean/doctl/pkg/ssh"
36	"github.com/digitalocean/godo"
37	"github.com/spf13/viper"
38	"golang.org/x/oauth2"
39)
40
41const (
42	// LatestReleaseURL is the latest release URL endpoint.
43	LatestReleaseURL = "https://api.github.com/repos/digitalocean/doctl/releases/latest"
44)
45
46// Version is the version info for doit.
47type Version struct {
48	Major, Minor, Patch int
49	Name, Build, Label  string
50}
51
52var (
53	// Build is set at build time. It defines the git SHA for the current
54	// build.
55	Build string
56
57	// Major is set at build time. It defines the major semantic version of
58	// doctl.
59	Major string
60
61	// Minor is set at build time. It defines the minor semantic version of
62	// doctl.
63	Minor string
64
65	// Patch is set at build time. It defines the patch semantic version of
66	// doctl.
67	Patch string
68
69	// Label is set at build time. It defines the string that comes after the
70	// version of doctl, ie, the "dev" in v1.0.0-dev.
71	Label string
72
73	// DoitVersion is doctl's version.
74	DoitVersion Version
75
76	// TraceHTTP traces http connections.
77	TraceHTTP bool
78)
79
80func init() {
81	if Build != "" {
82		DoitVersion.Build = Build
83	}
84	if Major != "" {
85		i, _ := strconv.Atoi(Major)
86		DoitVersion.Major = i
87	}
88	if Minor != "" {
89		i, _ := strconv.Atoi(Minor)
90		DoitVersion.Minor = i
91	}
92	if Patch != "" {
93		i, _ := strconv.Atoi(Patch)
94		DoitVersion.Patch = i
95	}
96	if Label == "" {
97		DoitVersion.Label = "dev"
98	} else {
99		DoitVersion.Label = Label
100	}
101}
102
103func (v Version) String() string {
104	var buffer bytes.Buffer
105	buffer.WriteString(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch))
106	if v.Label != "" {
107		buffer.WriteString("-" + v.Label)
108	}
109
110	return buffer.String()
111}
112
113// Complete is the complete version for doit.
114func (v Version) Complete(lv LatestVersioner) string {
115	var buffer bytes.Buffer
116	buffer.WriteString(fmt.Sprintf("doctl version %s", v.String()))
117
118	if v.Build != "" {
119		buffer.WriteString(fmt.Sprintf("\nGit commit hash: %s", v.Build))
120	}
121
122	if tagName, err := lv.LatestVersion(); err == nil {
123		v0, err1 := semver.Make(tagName)
124		v1, err2 := semver.Make(v.String())
125
126		if len(v0.Build) == 0 {
127			v0, err1 = semver.Make(tagName + "-release")
128		}
129
130		if err1 == nil && err2 == nil && v0.GT(v1) {
131			buffer.WriteString(fmt.Sprintf("\nrelease %s is available, check it out! ", tagName))
132		}
133	}
134
135	return buffer.String()
136}
137
138// LatestVersioner an interface for detecting the latest version.
139type LatestVersioner interface {
140	LatestVersion() (string, error)
141}
142
143// GithubLatestVersioner retrieves the latest version from GitHub.
144type GithubLatestVersioner struct{}
145
146var _ LatestVersioner = &GithubLatestVersioner{}
147
148// LatestVersion retrieves the latest version from Github or returns
149// an error.
150func (glv *GithubLatestVersioner) LatestVersion() (string, error) {
151	u := LatestReleaseURL
152	res, err := http.Get(u)
153	if err != nil {
154		return "", err
155	}
156
157	defer res.Body.Close()
158
159	var m map[string]interface{}
160	if err = json.NewDecoder(res.Body).Decode(&m); err != nil {
161		return "", err
162	}
163
164	tn, ok := m["tag_name"]
165	if !ok {
166		return "", errors.New("could not find tag name in response")
167	}
168
169	tagName := tn.(string)
170	return strings.TrimPrefix(tagName, "v"), nil
171}
172
173// Config is an interface that represent doit's config.
174type Config interface {
175	GetGodoClient(trace bool, accessToken string) (*godo.Client, error)
176	SSH(user, host, keyPath string, port int, opts ssh.Options) runner.Runner
177	Listen(url *url.URL, token string, schemaFunc listen.SchemaFunc, out io.Writer) listen.ListenerService
178	Set(ns, key string, val interface{})
179	IsSet(key string) bool
180	GetString(ns, key string) (string, error)
181	GetBool(ns, key string) (bool, error)
182	GetBoolPtr(ns, key string) (*bool, error)
183	GetInt(ns, key string) (int, error)
184	GetIntPtr(ns, key string) (*int, error)
185	GetStringSlice(ns, key string) ([]string, error)
186	GetStringMapString(ns, key string) (map[string]string, error)
187}
188
189// LiveConfig is an implementation of Config for live values.
190type LiveConfig struct {
191	cliArgs map[string]bool
192}
193
194var _ Config = &LiveConfig{}
195
196// GetGodoClient returns a GodoClient.
197func (c *LiveConfig) GetGodoClient(trace bool, accessToken string) (*godo.Client, error) {
198	if accessToken == "" {
199		return nil, fmt.Errorf("access token is required. (hint: run 'doctl auth init')")
200	}
201
202	tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken})
203	oauthClient := oauth2.NewClient(context.Background(), tokenSource)
204
205	if trace {
206		r := newRecorder(oauthClient.Transport)
207
208		go func() {
209			for {
210				select {
211				case msg := <-r.req:
212					log.Println("->", strconv.Quote(msg))
213				case msg := <-r.resp:
214					log.Println("<-", strconv.Quote(msg))
215				}
216			}
217		}()
218
219		oauthClient.Transport = r
220	}
221
222	args := []godo.ClientOpt{godo.SetUserAgent(userAgent())}
223
224	apiURL := viper.GetString("api-url")
225	if apiURL != "" {
226		args = append(args, godo.SetBaseURL(apiURL))
227	}
228
229	return godo.New(oauthClient, args...)
230}
231
232func userAgent() string {
233	return fmt.Sprintf("doctl/%s (%s %s)", DoitVersion.String(), runtime.GOOS, runtime.GOARCH)
234}
235
236// SSH creates a ssh connection to a host.
237func (c *LiveConfig) SSH(user, host, keyPath string, port int, opts ssh.Options) runner.Runner {
238	return &ssh.Runner{
239		User:            user,
240		Host:            host,
241		KeyPath:         keyPath,
242		Port:            port,
243		AgentForwarding: opts[ArgsSSHAgentForwarding].(bool),
244		Command:         opts[ArgSSHCommand].(string),
245	}
246}
247
248// Listen creates a websocket connection
249func (c *LiveConfig) Listen(url *url.URL, token string, schemaFunc listen.SchemaFunc, out io.Writer) listen.ListenerService {
250	return listen.NewListener(url, token, schemaFunc, out)
251}
252
253// Set sets a config key.
254func (c *LiveConfig) Set(ns, key string, val interface{}) {
255	viper.Set(nskey(ns, key), val)
256}
257
258// IsSet checks if a config is set
259func (c *LiveConfig) IsSet(key string) bool {
260	matches := regexp.MustCompile("\b*--([a-z-_]+)").FindAllStringSubmatch(strings.Join(os.Args, " "), -1)
261	if len(matches) == 0 {
262		return false
263	}
264
265	if len(c.cliArgs) == 0 {
266		args := make(map[string]bool)
267		for _, match := range matches {
268			args[match[1]] = true
269		}
270		c.cliArgs = args
271	}
272	return c.cliArgs[key]
273}
274
275// GetString returns a config value as a string.
276func (c *LiveConfig) GetString(ns, key string) (string, error) {
277	nskey := nskey(ns, key)
278	str := viper.GetString(nskey)
279
280	if isRequired(nskey) && strings.TrimSpace(str) == "" {
281		return "", NewMissingArgsErr(nskey)
282	}
283	return str, nil
284}
285
286// GetBool returns a config value as a bool.
287func (c *LiveConfig) GetBool(ns, key string) (bool, error) {
288	return viper.GetBool(nskey(ns, key)), nil
289}
290
291// GetBoolPtr returns a config value as a bool pointer.
292func (c *LiveConfig) GetBoolPtr(ns, key string) (*bool, error) {
293	if !c.IsSet(key) {
294		return nil, nil
295	}
296	val := viper.GetBool(nskey(ns, key))
297	return &val, nil
298}
299
300// GetInt returns a config value as an int.
301func (c *LiveConfig) GetInt(ns, key string) (int, error) {
302	nskey := nskey(ns, key)
303	val := viper.GetInt(nskey)
304
305	if isRequired(nskey) && val == 0 {
306		return 0, NewMissingArgsErr(nskey)
307	}
308	return val, nil
309}
310
311// GetIntPtr returns a config value as an int pointer.
312func (c *LiveConfig) GetIntPtr(ns, key string) (*int, error) {
313	nskey := nskey(ns, key)
314
315	if !c.IsSet(key) {
316		if isRequired(nskey) {
317			return nil, NewMissingArgsErr(nskey)
318		}
319		return nil, nil
320	}
321	val := viper.GetInt(nskey)
322	return &val, nil
323}
324
325// GetStringSlice returns a config value as a string slice.
326func (c *LiveConfig) GetStringSlice(ns, key string) ([]string, error) {
327	nskey := nskey(ns, key)
328	val := viper.GetStringSlice(nskey)
329
330	if isRequired(nskey) && emptyStringSlice(val) {
331		return nil, NewMissingArgsErr(nskey)
332	}
333
334	out := []string{}
335	for _, item := range viper.GetStringSlice(nskey) {
336		item = strings.TrimPrefix(item, "[")
337		item = strings.TrimSuffix(item, "]")
338
339		list := strings.Split(item, ",")
340		for _, str := range list {
341			if str == "" {
342				continue
343			}
344			out = append(out, str)
345		}
346	}
347	return out, nil
348}
349
350// GetStringMapString returns a config value as a string to string map.
351func (c *LiveConfig) GetStringMapString(ns, key string) (map[string]string, error) {
352	nskey := nskey(ns, key)
353
354	if isRequired(nskey) && !c.IsSet(key) {
355		return nil, NewMissingArgsErr(nskey)
356	}
357
358	// We cannot call viper.GetStringMapString because it does not handle
359	// pflag's StringToStringP properly:
360	// https://github.com/spf13/viper/issues/608
361	// Re-implement the necessary pieces on our own instead.
362
363	vals := map[string]string{}
364	items := viper.GetStringSlice(nskey)
365	for _, item := range items {
366		parts := strings.SplitN(item, "=", 2)
367		if len(parts) < 2 {
368			return nil, fmt.Errorf("item %q does not adhere to form: key=value", item)
369		}
370		labelKey := parts[0]
371		labelValue := parts[1]
372		vals[labelKey] = labelValue
373	}
374
375	return vals, nil
376}
377
378func nskey(ns, key string) string {
379	return fmt.Sprintf("%s.%s", ns, key)
380}
381
382func isRequired(key string) bool {
383	return viper.GetBool(fmt.Sprintf("required.%s", key))
384}
385
386// TestConfig is an implementation of Config for testing.
387type TestConfig struct {
388	SSHFn    func(user, host, keyPath string, port int, opts ssh.Options) runner.Runner
389	ListenFn func(url *url.URL, token string, schemaFunc listen.SchemaFunc, out io.Writer) listen.ListenerService
390	v        *viper.Viper
391	IsSetMap map[string]bool
392}
393
394var _ Config = &TestConfig{}
395
396// NewTestConfig creates a new, ready-to-use instance of a TestConfig.
397func NewTestConfig() *TestConfig {
398	return &TestConfig{
399		SSHFn: func(u, h, kp string, p int, opts ssh.Options) runner.Runner {
400			return &MockRunner{}
401		},
402		ListenFn: func(url *url.URL, token string, schemaFunc listen.SchemaFunc, out io.Writer) listen.ListenerService {
403			return &MockListener{}
404		},
405		v:        viper.New(),
406		IsSetMap: make(map[string]bool),
407	}
408}
409
410// GetGodoClient mocks a GetGodoClient call. The returned godo client will
411// be nil.
412func (c *TestConfig) GetGodoClient(trace bool, accessToken string) (*godo.Client, error) {
413	return &godo.Client{}, nil
414}
415
416// SSH returns a mock SSH runner.
417func (c *TestConfig) SSH(user, host, keyPath string, port int, opts ssh.Options) runner.Runner {
418	return c.SSHFn(user, host, keyPath, port, opts)
419}
420
421// Listen returns a mock websocket listener
422func (c *TestConfig) Listen(url *url.URL, token string, schemaFunc listen.SchemaFunc, out io.Writer) listen.ListenerService {
423	return c.ListenFn(url, token, schemaFunc, out)
424}
425
426// Set sets a config key.
427func (c *TestConfig) Set(ns, key string, val interface{}) {
428	nskey := fmt.Sprintf("%s-%s", ns, key)
429	c.v.Set(nskey, val)
430	c.IsSetMap[key] = true
431}
432
433// IsSet returns true if the given key is set on the config.
434func (c *TestConfig) IsSet(key string) bool {
435	return c.IsSetMap[key]
436}
437
438// GetString returns the string value for the key in the given namespace. Because
439// this is a mock implementation, and error will never be returned.
440func (c *TestConfig) GetString(ns, key string) (string, error) {
441	nskey := fmt.Sprintf("%s-%s", ns, key)
442	return c.v.GetString(nskey), nil
443}
444
445// GetInt returns the int value for the key in the given namespace. Because
446// this is a mock implementation, and error will never be returned.
447func (c *TestConfig) GetInt(ns, key string) (int, error) {
448	nskey := fmt.Sprintf("%s-%s", ns, key)
449	return c.v.GetInt(nskey), nil
450}
451
452// GetIntPtr returns the int value for the key in the given namespace. Because
453// this is a mock implementation, and error will never be returned.
454func (c *TestConfig) GetIntPtr(ns, key string) (*int, error) {
455	nskey := fmt.Sprintf("%s-%s", ns, key)
456	if !c.v.IsSet(nskey) {
457		return nil, nil
458	}
459	val := c.v.GetInt(nskey)
460	return &val, nil
461}
462
463// GetStringSlice returns the string slice value for the key in the given
464// namespace. Because this is a mock implementation, and error will never be
465// returned.
466func (c *TestConfig) GetStringSlice(ns, key string) ([]string, error) {
467	nskey := fmt.Sprintf("%s-%s", ns, key)
468	return c.v.GetStringSlice(nskey), nil
469}
470
471// GetStringMapString returns the string-to-string value for the key in the
472// given namespace. Because this is a mock implementation, and error will never
473// be returned.
474func (c *TestConfig) GetStringMapString(ns, key string) (map[string]string, error) {
475	nskey := fmt.Sprintf("%s-%s", ns, key)
476	return c.v.GetStringMapString(nskey), nil
477}
478
479// GetBool returns the bool value for the key in the given namespace. Because
480// this is a mock implementation, and error will never be returned.
481func (c *TestConfig) GetBool(ns, key string) (bool, error) {
482	nskey := fmt.Sprintf("%s-%s", ns, key)
483	return c.v.GetBool(nskey), nil
484}
485
486// GetBoolPtr returns the bool value for the key in the given namespace. Because
487// this is a mock implementation, and error will never be returned.
488func (c *TestConfig) GetBoolPtr(ns, key string) (*bool, error) {
489	nskey := fmt.Sprintf("%s-%s", ns, key)
490	if !c.v.IsSet(nskey) {
491		return nil, nil
492	}
493	val := c.v.GetBool(nskey)
494	return &val, nil
495}
496
497// This is needed because an empty StringSlice flag returns `"[]"`
498func emptyStringSlice(s []string) bool {
499	return len(s) == 1 && s[0] == "[]"
500}
501
502// CommandName returns the name by which doctl was invoked
503func CommandName() string {
504	name, ok := os.LookupEnv("SNAP_NAME")
505	if !ok || name != "doctl" {
506		return os.Args[0]
507	}
508	return name
509}
510