A command line tool for DigitalOcean services

doit.go 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. /*
  2. Copyright 2016 The Doctl Authors All rights reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package doit
  14. import (
  15. "bytes"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "log"
  20. "net/http"
  21. "strconv"
  22. "strings"
  23. "github.com/blang/semver"
  24. "github.com/digitalocean/doctl/pkg/runner"
  25. "github.com/digitalocean/doctl/pkg/ssh"
  26. "github.com/digitalocean/godo"
  27. jww "github.com/spf13/jwalterweatherman"
  28. "github.com/spf13/viper"
  29. "golang.org/x/oauth2"
  30. )
  31. const (
  32. // NSRoot is a configuration key that signifies this value is at the root.
  33. NSRoot = "doit"
  34. // LatestReleaseURL is the latest release URL endpoint.
  35. LatestReleaseURL = "https://api.github.com/repos/bryanl/doit/releases/latest"
  36. )
  37. var (
  38. // DoitConfig holds the app's current configuration.
  39. DoitConfig Config = &LiveConfig{}
  40. // DoitVersion is doit's version.
  41. DoitVersion = Version{
  42. Major: 1,
  43. Minor: 0,
  44. Patch: 0,
  45. Label: "dev",
  46. }
  47. // Build is doit's build tag.
  48. Build string
  49. // Major is doctl's major version.
  50. Major string
  51. // Minor is doctl's minor version.
  52. Minor string
  53. // Patch is doctl's patch version.
  54. Patch string
  55. // Label is doctl's label.
  56. Label string
  57. // TraceHTTP traces http connections.
  58. TraceHTTP bool
  59. )
  60. func init() {
  61. jww.SetStdoutThreshold(jww.LevelError)
  62. }
  63. // Version is the version info for doit.
  64. type Version struct {
  65. Major, Minor, Patch int
  66. Name, Build, Label string
  67. }
  68. func (v Version) String() string {
  69. var buffer bytes.Buffer
  70. buffer.WriteString(fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch))
  71. if v.Label != "" {
  72. buffer.WriteString("-" + v.Label)
  73. }
  74. return buffer.String()
  75. }
  76. // Complete is the complete version for doit.
  77. func (v Version) Complete(lv LatestVersioner) string {
  78. var buffer bytes.Buffer
  79. buffer.WriteString(fmt.Sprintf("doctl version %s", v.String()))
  80. if v.Build != "" {
  81. buffer.WriteString(fmt.Sprintf("\nGit commit hash: %s", v.Build))
  82. }
  83. if tagName, err := lv.LatestVersion(); err == nil {
  84. v0, err1 := semver.Make(tagName)
  85. v1, err2 := semver.Make(v.String())
  86. if err1 == nil && err2 == nil && v0.GT(v1) {
  87. buffer.WriteString(fmt.Sprintf("\n%q is a newer release than %q", tagName, v.String()))
  88. }
  89. }
  90. return buffer.String()
  91. }
  92. // LatestVersioner an interface for detecting the latest version.
  93. type LatestVersioner interface {
  94. LatestVersion() (string, error)
  95. }
  96. // GithubLatestVersioner retrieves the latest version from Github.
  97. type GithubLatestVersioner struct{}
  98. var _ LatestVersioner = &GithubLatestVersioner{}
  99. // LatestVersion retrieves the latest version from Github or returns
  100. // an error.
  101. func (glv *GithubLatestVersioner) LatestVersion() (string, error) {
  102. u := LatestReleaseURL
  103. res, err := http.Get(u)
  104. if err != nil {
  105. return "", err
  106. }
  107. defer res.Body.Close()
  108. var m map[string]interface{}
  109. if err = json.NewDecoder(res.Body).Decode(&m); err != nil {
  110. return "", err
  111. }
  112. tn, ok := m["tag_name"]
  113. if !ok {
  114. return "", errors.New("could not find tag name in response")
  115. }
  116. tagName := tn.(string)
  117. return strings.TrimPrefix(tagName, "v"), nil
  118. }
  119. // Config is an interface that represent doit's config.
  120. type Config interface {
  121. GetGodoClient(trace bool) *godo.Client
  122. SSH(user, host, keyPath string, port int) runner.Runner
  123. Set(ns, key string, val interface{})
  124. GetString(ns, key string) (string, error)
  125. GetBool(ns, key string) (bool, error)
  126. GetInt(ns, key string) (int, error)
  127. GetStringSlice(ns, key string) ([]string, error)
  128. }
  129. // LiveConfig is an implementation of Config for live values.
  130. type LiveConfig struct {
  131. godoClient *godo.Client
  132. }
  133. var _ Config = &LiveConfig{}
  134. // GetGodoClient returns a GodoClient.
  135. func (c *LiveConfig) GetGodoClient(trace bool) *godo.Client {
  136. if c.godoClient != nil {
  137. return c.godoClient
  138. }
  139. token := viper.GetString("access-token")
  140. tokenSource := &TokenSource{AccessToken: token}
  141. oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource)
  142. if trace {
  143. r := newRecorder(oauthClient.Transport)
  144. go func() {
  145. for {
  146. select {
  147. case msg := <-r.req:
  148. log.Println("->", strconv.Quote(msg))
  149. case msg := <-r.resp:
  150. log.Println("<-", strconv.Quote(msg))
  151. }
  152. }
  153. }()
  154. oauthClient.Transport = r
  155. }
  156. c.godoClient = godo.NewClient(oauthClient)
  157. return c.godoClient
  158. }
  159. // SSH creates a ssh connection to a host.
  160. func (c *LiveConfig) SSH(user, host, keyPath string, port int) runner.Runner {
  161. return &ssh.Runner{
  162. User: user,
  163. Host: host,
  164. KeyPath: keyPath,
  165. Port: port,
  166. }
  167. }
  168. // Set sets a config key.
  169. func (c *LiveConfig) Set(ns, key string, val interface{}) {
  170. nskey := fmt.Sprintf("%s-%s", ns, key)
  171. viper.Set(nskey, val)
  172. }
  173. // GetString returns a config value as a string.
  174. func (c *LiveConfig) GetString(ns, key string) (string, error) {
  175. if ns == NSRoot {
  176. return viper.GetString(key), nil
  177. }
  178. nskey := fmt.Sprintf("%s.%s", ns, key)
  179. if _, ok := viper.AllSettings()[fmt.Sprintf("%s.required", nskey)]; ok {
  180. if viper.GetString(nskey) == "" {
  181. return "", NewMissingArgsErr(nskey)
  182. }
  183. }
  184. return viper.GetString(nskey), nil
  185. }
  186. // GetBool returns a config value as a bool.
  187. func (c *LiveConfig) GetBool(ns, key string) (bool, error) {
  188. if ns == NSRoot {
  189. return viper.GetBool(key), nil
  190. }
  191. nskey := fmt.Sprintf("%s.%s", ns, key)
  192. return viper.GetBool(nskey), nil
  193. }
  194. // GetInt returns a config value as an int.
  195. func (c *LiveConfig) GetInt(ns, key string) (int, error) {
  196. if ns == NSRoot {
  197. return viper.GetInt(key), nil
  198. }
  199. nskey := fmt.Sprintf("%s.%s", ns, key)
  200. if _, ok := viper.AllSettings()[fmt.Sprintf("%s.required", nskey)]; ok {
  201. if viper.GetInt(nskey) < 0 {
  202. return 0, NewMissingArgsErr(nskey)
  203. }
  204. }
  205. return viper.GetInt(nskey), nil
  206. }
  207. // GetStringSlice returns a config value as a string slice.
  208. func (c *LiveConfig) GetStringSlice(ns, key string) ([]string, error) {
  209. if ns == NSRoot {
  210. return viper.GetStringSlice(key), nil
  211. }
  212. nskey := fmt.Sprintf("%s.%s", ns, key)
  213. if _, ok := viper.AllSettings()[fmt.Sprintf("%s.required", nskey)]; ok {
  214. if viper.GetStringSlice(nskey) == nil {
  215. return nil, NewMissingArgsErr(nskey)
  216. }
  217. }
  218. return viper.GetStringSlice(nskey), nil
  219. }