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