diff --git a/command/login.go b/command/login.go
new file mode 100644
index 000000000..527d43643
--- /dev/null
+++ b/command/login.go
@@ -0,0 +1,595 @@
+package command
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "math/rand"
+ "net"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "github.com/hashicorp/terraform/command/cliconfig"
+ "github.com/hashicorp/terraform/httpclient"
+ "github.com/hashicorp/terraform/svchost"
+ svcauth "github.com/hashicorp/terraform/svchost/auth"
+ "github.com/hashicorp/terraform/svchost/disco"
+ "github.com/hashicorp/terraform/tfdiags"
+
+ uuid "github.com/hashicorp/go-uuid"
+ "golang.org/x/oauth2"
+)
+
+// LoginCommand is a Command implementation that runs an interactive login
+// flow for a remote service host. It then stashes credentials in a tfrc
+// file in the user's home directory.
+type LoginCommand struct {
+ Meta
+}
+
+// Run implements cli.Command.
+func (c *LoginCommand) Run(args []string) int {
+ args, err := c.Meta.process(args, false)
+ if err != nil {
+ return 1
+ }
+
+ cmdFlags := c.Meta.extendedFlagSet("login")
+ cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
+ if err := cmdFlags.Parse(args); err != nil {
+ return 1
+ }
+
+ args = cmdFlags.Args()
+ if len(args) > 1 {
+ c.Ui.Error(
+ "The login command expects at most one argument: the host to log in to.")
+ cmdFlags.Usage()
+ return 1
+ }
+
+ var diags tfdiags.Diagnostics
+
+ if !c.input {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Login is an interactive command",
+ "The \"terraform login\" command uses interactive prompts to obtain and record credentials, so it can't be run with input disabled.\n\nTo configure credentials in a non-interactive context, write existing credentials directly to a CLI configuration file.",
+ ))
+ c.showDiagnostics(diags)
+ return 1
+ }
+
+ givenHostname := "app.terraform.io"
+ if len(args) != 0 {
+ givenHostname = args[0]
+ }
+
+ hostname, err := svchost.ForComparison(givenHostname)
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Invalid hostname",
+ fmt.Sprintf("The given hostname %q is not valid: %s.", givenHostname, err.Error()),
+ ))
+ c.showDiagnostics(diags)
+ return 1
+ }
+
+ // From now on, since we've validated the given hostname, we should use
+ // dispHostname in the UI to ensure we're presenting it in the canonical
+ // form, in case that helpers users with debugging when things aren't
+ // working as expected. (Perhaps the normalization is part of the cause.)
+ dispHostname := hostname.ForDisplay()
+
+ host, err := c.Services.Discover(hostname)
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Service discovery failed for "+dispHostname,
+
+ // Contrary to usual Go idiom, the Discover function returns
+ // full sentences with initial capitalization in its error messages,
+ // and they are written with the end-user as the audience. We
+ // only need to add the trailing period to make them consistent
+ // with our usual error reporting standards.
+ err.Error()+".",
+ ))
+ c.showDiagnostics(diags)
+ return 1
+ }
+
+ creds := c.Services.CredentialsSource()
+
+ // In normal use (i.e. without test mocks/fakes) creds will be an instance
+ // of the command/cliconfig.CredentialsSource type, which has some extra
+ // methods we can use to give the user better feedback about what we're
+ // going to do. credsCtx will be nil if it's any other implementation,
+ // though.
+ var credsCtx *loginCredentialsContext
+ if c, ok := creds.(*cliconfig.CredentialsSource); ok {
+ filename, _ := c.CredentialsFilePath()
+ credsCtx = &loginCredentialsContext{
+ Location: c.HostCredentialsLocation(hostname),
+ LocalFilename: filename, // empty in the very unlikely event that we can't select a config directory for this user
+ HelperType: c.CredentialsHelperType(),
+ }
+ }
+
+ clientConfig, err := host.ServiceOAuthClient("login.v1")
+ switch err.(type) {
+ case nil:
+ // Great! No problem, then.
+ case *disco.ErrServiceNotProvided:
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Host does not support Terraform login",
+ fmt.Sprintf("The given hostname %q does not allow creating Terraform authorization tokens.", dispHostname),
+ ))
+ case *disco.ErrVersionNotSupported:
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Host does not support Terraform login",
+ fmt.Sprintf("The given hostname %q allows creating Terraform authorization tokens, but requires a newer version of Terraform CLI to do so.", dispHostname),
+ ))
+ default:
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Host does not support Terraform login",
+ fmt.Sprintf("The given hostname %q cannot support \"terraform login\": %s.", dispHostname, err),
+ ))
+ }
+
+ if credsCtx.Location == cliconfig.CredentialsInOtherFile {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ fmt.Sprintf("Credentials for %s are manually configured", dispHostname),
+ "The \"terraform login\" command cannot log in because credentials for this host are already configured in a CLI configuration file.\n\nTo log in, first revoke the existing credentials and remove that block from the CLI configuration.",
+ ))
+ }
+
+ if diags.HasErrors() {
+ c.showDiagnostics(diags)
+ return 1
+ }
+
+ var token *oauth2.Token
+ switch {
+ case clientConfig.SupportedGrantTypes.Has(disco.OAuthAuthzCodeGrant):
+ // We prefer an OAuth code grant if the server supports it.
+ var tokenDiags tfdiags.Diagnostics
+ token, tokenDiags = c.interactiveGetTokenByCode(hostname, credsCtx, clientConfig)
+ diags = diags.Append(tokenDiags)
+ if tokenDiags.HasErrors() {
+ c.showDiagnostics(diags)
+ return 1
+ }
+ case clientConfig.SupportedGrantTypes.Has(disco.OAuthOwnerPasswordGrant) && hostname == svchost.Hostname("app.terraform.io"):
+ // The password grant type is allowed only for Terraform Cloud SaaS.
+ var tokenDiags tfdiags.Diagnostics
+ token, tokenDiags = c.interactiveGetTokenByPassword(hostname, credsCtx, clientConfig)
+ diags = diags.Append(tokenDiags)
+ if tokenDiags.HasErrors() {
+ c.showDiagnostics(diags)
+ return 1
+ }
+ default:
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Host does not support Terraform login",
+ fmt.Sprintf("The given hostname %q does not allow any OAuth grant types that are supported by this version of Terraform.", dispHostname),
+ ))
+ c.showDiagnostics(diags)
+ return 1
+ }
+
+ err = creds.StoreForHost(hostname, svcauth.HostCredentialsToken(token.AccessToken))
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Failed to save API token",
+ fmt.Sprintf("The given host returned an API token, but Terraform failed to save it: %s.", err),
+ ))
+ }
+
+ c.showDiagnostics(diags)
+ if diags.HasErrors() {
+ return 1
+ }
+
+ c.Ui.Output("\n---------------------------------------------------------------------------------\n")
+ c.Ui.Output(
+ fmt.Sprintf(
+ c.Colorize().Color(strings.TrimSpace(`
+[green][bold]Success![reset] [bold]Terraform has obtained and saved an API token.[reset]
+
+The new API token will be used for any future Terraform command that must make
+authenticated requests to %s.
+`)),
+ dispHostname,
+ ) + "\n",
+ )
+
+ return 0
+}
+
+// Help implements cli.Command.
+func (c *LoginCommand) Help() string {
+ defaultFile := c.defaultOutputFile()
+ if defaultFile == "" {
+ // Because this is just for the help message and it's very unlikely
+ // that a user wouldn't have a functioning home directory anyway,
+ // we'll just use a placeholder here. The real command has some
+ // more complex behavior for this case. This result is not correct
+ // on all platforms, but given how unlikely we are to hit this case
+ // that seems okay.
+ defaultFile = "~/.terraform/credentials.tfrc.json"
+ }
+
+ helpText := fmt.Sprintf(`
+Usage: terraform login [hostname]
+
+ Retrieves an authentication token for the given hostname, if it supports
+ automatic login, and saves it in a credentials file in your home directory.
+
+ If no hostname is provided, the default hostname is app.terraform.io, to
+ log in to Terraform Cloud.
+
+ If not overridden by credentials helper settings in the CLI configuration,
+ the credentials will be written to the following local file:
+ %s
+`, defaultFile)
+ return strings.TrimSpace(helpText)
+}
+
+// Synopsis implements cli.Command.
+func (c *LoginCommand) Synopsis() string {
+ return "Obtain and save credentials for a remote host"
+}
+
+func (c *LoginCommand) defaultOutputFile() string {
+ if c.CLIConfigDir == "" {
+ return "" // no default available
+ }
+ return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json")
+}
+
+func (c *LoginCommand) interactiveGetTokenByCode(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
+ var diags tfdiags.Diagnostics
+
+ confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthAuthzCodeGrant, credsCtx)
+ diags = diags.Append(confirmDiags)
+ if !confirm {
+ diags = diags.Append(errors.New("Login cancelled"))
+ return nil, diags
+ }
+
+ // We'll use an entirely pseudo-random UUID for our temporary request
+ // state. The OAuth server must echo this back to us in the callback
+ // request to make it difficult for some other running process to
+ // interfere by sending its own request to our temporary server.
+ reqState, err := uuid.GenerateUUID()
+ if err != nil {
+ // This should be very unlikely, but could potentially occur if e.g.
+ // there's not enough pseudo-random entropy available.
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Can't generate login request state",
+ fmt.Sprintf("Cannot generate random request identifier for login request: %s.", err),
+ ))
+ return nil, diags
+ }
+
+ proofKey, proofKeyChallenge, err := c.proofKey()
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Can't generate login request state",
+ fmt.Sprintf("Cannot generate random prrof key for login request: %s.", err),
+ ))
+ return nil, diags
+ }
+
+ listener, callbackURL, err := c.listenerForCallback(clientConfig.MinPort, clientConfig.MaxPort)
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Can't start temporary login server",
+ fmt.Sprintf(
+ "The login process uses OAuth, which requires starting a temporary HTTP server on localhost. However, no TCP port numbers between %d and %d are available to create such a server.",
+ clientConfig.MinPort, clientConfig.MaxPort,
+ ),
+ ))
+ return nil, diags
+ }
+
+ // codeCh will allow our temporary HTTP server to transmit the OAuth code
+ // to the main execution path that follows.
+ codeCh := make(chan string)
+ server := &http.Server{
+ Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ log.Printf("[TRACE] login: request to callback server")
+ err := req.ParseForm()
+ if err != nil {
+ log.Printf("[ERROR] login: cannot ParseForm on callback request: %s", err)
+ resp.WriteHeader(400)
+ return
+ }
+ gotState := req.Form.Get("state")
+ if gotState != reqState {
+ log.Printf("[ERROR] login: incorrect \"state\" value in callback request")
+ resp.WriteHeader(400)
+ return
+ }
+ gotCode := req.Form.Get("code")
+ if gotCode == "" {
+ log.Printf("[ERROR] login: no \"code\" argument in callback request")
+ resp.WriteHeader(400)
+ return
+ }
+
+ log.Printf("[TRACE] login: request contains an authorization code")
+
+ // Send the code to our blocking wait below, so that the token
+ // fetching process can continue.
+ codeCh <- gotCode
+ close(codeCh)
+
+ log.Printf("[TRACE] login: returning response from callback server")
+
+ resp.Header().Add("Content-Type", "text/html")
+ resp.WriteHeader(200)
+ resp.Write([]byte(callbackSuccessMessage))
+ }),
+ }
+ go func() {
+ err = server.Serve(listener)
+ if err != nil && err != http.ErrServerClosed {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Can't start temporary login server",
+ fmt.Sprintf(
+ "The login process uses OAuth, which requires starting a temporary HTTP server on localhost. However, no TCP port numbers between %d and %d are available to create such a server.",
+ clientConfig.MinPort, clientConfig.MaxPort,
+ ),
+ ))
+ close(codeCh)
+ }
+ }()
+
+ oauthConfig := &oauth2.Config{
+ ClientID: clientConfig.ID,
+ Endpoint: clientConfig.Endpoint(),
+ RedirectURL: callbackURL,
+ }
+
+ authCodeURL := oauthConfig.AuthCodeURL(
+ reqState,
+ oauth2.SetAuthURLParam("code_challenge", proofKeyChallenge),
+ oauth2.SetAuthURLParam("code_challenge_method", "S256"),
+ )
+
+ launchBrowserManually := false
+ if c.BrowserLauncher != nil {
+ err = c.BrowserLauncher.OpenURL(authCodeURL)
+ if err == nil {
+ c.Ui.Output(fmt.Sprintf("Terraform must now open a web browser to the login page for %s.\n", hostname.ForDisplay()))
+ c.Ui.Output(fmt.Sprintf("If a browser does not open this automatically, open the following URL to proceed:\n %s\n", authCodeURL))
+ } else {
+ // Assume we're on a platform where opening a browser isn't possible.
+ launchBrowserManually = true
+ }
+ } else {
+ launchBrowserManually = true
+ }
+
+ if launchBrowserManually {
+ c.Ui.Output(fmt.Sprintf("Open the following URL to access the login page for %s:\n %s\n", hostname.ForDisplay(), authCodeURL))
+ }
+
+ c.Ui.Output("Terraform will now wait for the host to signal that login was successful.\n")
+
+ code, ok := <-codeCh
+ if !ok {
+ // If we got no code at all then the server wasn't able to start
+ // up, so we'll just give up.
+ return nil, diags
+ }
+
+ err = server.Close()
+ if err != nil {
+ // The server will close soon enough when our process exits anyway,
+ // so we won't fuss about it for right now.
+ log.Printf("[WARN] login: callback server can't shut down: %s", err)
+ }
+
+ ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpclient.New())
+ token, err := oauthConfig.Exchange(
+ ctx, code,
+ oauth2.SetAuthURLParam("code_verifier", proofKey),
+ )
+ if err != nil {
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Failed to obtain auth token",
+ fmt.Sprintf("The remote server did not assign an auth token: %s.", err),
+ ))
+ return nil, diags
+ }
+
+ return token, diags
+}
+
+func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
+ var diags tfdiags.Diagnostics
+
+ confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthOwnerPasswordGrant, credsCtx)
+ diags = diags.Append(confirmDiags)
+ if !confirm {
+ diags = diags.Append(errors.New("Login cancelled"))
+ return nil, diags
+ }
+
+ c.Ui.Output("\n---------------------------------------------------------------------------------\n")
+ c.Ui.Output("Terraform must temporarily use your password to request an API token.\nThis password will NOT be saved locally.\n")
+
+ username, err := c.Ui.Ask(fmt.Sprintf("Username for %s:", hostname.ForDisplay()))
+ if err != nil {
+ diags = diags.Append(fmt.Errorf("Failed to request username: %s", err))
+ return nil, diags
+ }
+ password, err := c.Ui.AskSecret(fmt.Sprintf("Password for %s:", username))
+ if err != nil {
+ diags = diags.Append(fmt.Errorf("Failed to request password: %s", err))
+ return nil, diags
+ }
+
+ oauthConfig := &oauth2.Config{
+ ClientID: clientConfig.ID,
+ Endpoint: clientConfig.Endpoint(),
+ }
+ token, err := oauthConfig.PasswordCredentialsToken(context.Background(), username, password)
+ if err != nil {
+ // FIXME: The OAuth2 library generates errors that are not appropriate
+ // for a Terraform end-user audience, so once we have more experience
+ // with which errors are most common we should try to recognize them
+ // here and produce better error messages for them.
+ diags = diags.Append(tfdiags.Sourceless(
+ tfdiags.Error,
+ "Failed to retrieve API token",
+ fmt.Sprintf("The remote host did not issue an API token: %s.", err),
+ ))
+ }
+
+ return token, diags
+}
+
+func (c *LoginCommand) interactiveContextConsent(hostname svchost.Hostname, grantType disco.OAuthGrantType, credsCtx *loginCredentialsContext) (bool, tfdiags.Diagnostics) {
+ var diags tfdiags.Diagnostics
+
+ c.Ui.Output(fmt.Sprintf("Terraform will request an API token for %s using OAuth.\n", hostname.ForDisplay()))
+
+ if grantType.UsesAuthorizationEndpoint() {
+ c.Ui.Output(
+ "This will work only if you are able to use a web browser on this computer to\ncomplete a login process. If not, you must obtain an API token by another\nmeans and configure it in the CLI configuration manually.\n",
+ )
+ }
+
+ // credsCtx might not be set if we're using a mock credentials source
+ // in a test, but it should always be set in normal use.
+ if credsCtx != nil {
+ switch credsCtx.Location {
+ case cliconfig.CredentialsViaHelper:
+ c.Ui.Output(fmt.Sprintf("If login is successful, Terraform will store the token in the configured\n%q credentials helper for use by subsequent commands.\n", credsCtx.HelperType))
+ case cliconfig.CredentialsInPrimaryFile, cliconfig.CredentialsNotAvailable:
+ c.Ui.Output(fmt.Sprintf("If login is successful, Terraform will store the token in plain text in\nthe following file for use by subsequent commands:\n %s\n", credsCtx.LocalFilename))
+ }
+ }
+
+ v, err := c.Ui.Ask("Do you want to proceed? (y/n)")
+ if err != nil {
+ // Should not happen because this command checks that input is enabled
+ // before we get to this point.
+ diags = diags.Append(err)
+ return false, diags
+ }
+
+ switch strings.ToLower(v) {
+ case "y", "yes":
+ return true, diags
+ default:
+ return false, diags
+ }
+}
+
+func (c *LoginCommand) listenerForCallback(minPort, maxPort uint16) (net.Listener, string, error) {
+ if minPort < 1024 || maxPort < 1024 {
+ // This should never happen because it should've been checked by
+ // the svchost/disco package when reading the service description,
+ // but we'll prefer to fail hard rather than inadvertently trying
+ // to open an unprivileged port if there are bugs at that layer.
+ panic("listenerForCallback called with privileged port number")
+ }
+
+ availCount := int(maxPort) - int(minPort)
+
+ // We're going to try port numbers within the range at random, so we need
+ // to terminate eventually in case _none_ of the ports are available.
+ // We'll make that 150% of the number of ports just to give us some room
+ // for the random number generator to generate the same port more than
+ // once.
+ // Note that we don't really care about true randomness here... we're just
+ // trying to hop around in the available port space rather than always
+ // working up from the lowest, because we have no information to predict
+ // that any particular number will be more likely to be available than
+ // another.
+ maxTries := availCount + (availCount / 2)
+
+ for tries := 0; tries < maxTries; tries++ {
+ port := rand.Intn(availCount) + int(minPort)
+ addr := fmt.Sprintf("127.0.0.1:%d", port)
+ log.Printf("[TRACE] login: trying %s as a listen address for temporary OAuth callback server", addr)
+ l, err := net.Listen("tcp4", addr)
+ if err == nil {
+ // We use a path that doesn't end in a slash here because some
+ // OAuth server implementations don't allow callback URLs to
+ // end with slashes.
+ callbackURL := fmt.Sprintf("http://localhost:%d/login", port)
+ log.Printf("[TRACE] login: callback URL will be %s", callbackURL)
+ return l, callbackURL, nil
+ }
+ }
+
+ return nil, "", fmt.Errorf("no suitable TCP ports (between %d and %d) are available for the temporary OAuth callback server", minPort, maxPort)
+}
+
+func (c *LoginCommand) proofKey() (key, challenge string, err error) {
+ // Wel use a UUID-like string as the "proof key for code exchange" (PKCE)
+ // that will eventually authenticate our request to the token endpoint.
+ // Standard UUIDs are explicitly not suitable as secrets according to the
+ // UUID spec, but our go-uuid just generates totally random number sequences
+ // formatted in the conventional UUID syntax, so that concern does not
+ // apply here: this is just a 128-bit crypto-random number.
+ key, err = uuid.GenerateUUID()
+ if err != nil {
+ return "", "", err
+ }
+
+ h := sha256.New()
+ h.Write([]byte(key))
+ challenge = base64.URLEncoding.EncodeToString(h.Sum(nil))
+
+ return key, challenge, nil
+}
+
+type loginCredentialsContext struct {
+ Location cliconfig.CredentialsLocation
+ LocalFilename string
+ HelperType string
+}
+
+const callbackSuccessMessage = `
+
+
+Terraform Login
+
+
+
+
+The login server has returned an authentication code to Terraform.
+Now close this page and return to the terminal where terraform login
+is running to see the result of the login process.
+
+
+
+`
diff --git a/command/login_test.go b/command/login_test.go
new file mode 100644
index 000000000..33d68cb5a
--- /dev/null
+++ b/command/login_test.go
@@ -0,0 +1,135 @@
+package command
+
+import (
+ "bytes"
+ "context"
+ "io/ioutil"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/mitchellh/cli"
+
+ "github.com/hashicorp/terraform/command/cliconfig"
+ oauthserver "github.com/hashicorp/terraform/command/testdata/login-oauth-server"
+ "github.com/hashicorp/terraform/command/webbrowser"
+ "github.com/hashicorp/terraform/svchost"
+ "github.com/hashicorp/terraform/svchost/disco"
+)
+
+func TestLogin(t *testing.T) {
+ // oauthserver.Handler is a stub OAuth server implementation that will,
+ // on success, always issue a bearer token named "good-token".
+ s := httptest.NewServer(oauthserver.Handler)
+ defer s.Close()
+
+ loginTestCase := func(test func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string))) func(t *testing.T) {
+ return func(t *testing.T) {
+ t.Helper()
+ workDir, err := ioutil.TempDir("", "terraform-test-command-login")
+ if err != nil {
+ t.Fatalf("cannot create temporary directory: %s", err)
+ }
+ defer os.RemoveAll(workDir)
+
+ // We'll use this context to avoid asynchronous tasks outliving
+ // a single test run.
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ ui := cli.NewMockUi()
+ browserLauncher := webbrowser.NewMockLauncher(ctx)
+ creds := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json"))
+ svcs := disco.NewWithCredentialsSource(creds)
+
+ inputBuf := &bytes.Buffer{}
+ ui.InputReader = inputBuf
+
+ svcs.ForceHostServices(svchost.Hostname("app.terraform.io"), map[string]interface{}{
+ "login.v1": map[string]interface{}{
+ // On app.terraform.io we use password-based authorization.
+ // That's the only hostname that it's permitted for, so we can't
+ // use a fake hostname here.
+ "client": "terraformcli",
+ "token": s.URL + "/token",
+ "grant_types": []interface{}{"password"},
+ },
+ })
+ svcs.ForceHostServices(svchost.Hostname("example.com"), map[string]interface{}{
+ "login.v1": map[string]interface{}{
+ // For this fake hostname we'll use a conventional OAuth flow,
+ // with browser-based consent that we'll mock away using a
+ // mock browser launcher below.
+ "client": "anything-goes",
+ "authz": s.URL + "/authz",
+ "token": s.URL + "/token",
+ },
+ })
+ svcs.ForceHostServices(svchost.Hostname("unsupported.example.net"), map[string]interface{}{
+ // This host intentionally left blank.
+ })
+
+ c := &LoginCommand{
+ Meta: Meta{
+ Ui: ui,
+ BrowserLauncher: browserLauncher,
+ Services: svcs,
+ },
+ }
+
+ test(t, c, ui, func(data string) {
+ t.Helper()
+ inputBuf.WriteString(data)
+ })
+ }
+ }
+
+ t.Run("defaulting to app.terraform.io with password flow", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
+ // Enter "yes" at the consent prompt, then a username and then a password.
+ inp("yes\nfoo\nbar\n")
+ status := c.Run(nil)
+ if status != 0 {
+ t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
+ }
+
+ credsSrc := c.Services.CredentialsSource()
+ creds, err := credsSrc.ForHost(svchost.Hostname("app.terraform.io"))
+ if err != nil {
+ t.Errorf("failed to retrieve credentials: %s", err)
+ }
+ if got, want := creds.Token(), "good-token"; got != want {
+ t.Errorf("wrong token %q; want %q", got, want)
+ }
+ }))
+
+ t.Run("example.com with authorization code flow", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
+ // Enter "yes" at the consent prompt.
+ inp("yes\n")
+ status := c.Run([]string{"example.com"})
+ if status != 0 {
+ t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
+ }
+
+ credsSrc := c.Services.CredentialsSource()
+ creds, err := credsSrc.ForHost(svchost.Hostname("example.com"))
+ if err != nil {
+ t.Errorf("failed to retrieve credentials: %s", err)
+ }
+ if got, want := creds.Token(), "good-token"; got != want {
+ t.Errorf("wrong token %q; want %q", got, want)
+ }
+ }))
+
+ t.Run("host without login support", loginTestCase(func(t *testing.T, c *LoginCommand, ui *cli.MockUi, inp func(string)) {
+ status := c.Run([]string{"unsupported.example.net"})
+ if status == 0 {
+ t.Fatalf("successful exit; want error")
+ }
+
+ if got, want := ui.ErrorWriter.String(), "Error: Host does not support Terraform login"; !strings.Contains(got, want) {
+ t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
+ }
+ }))
+}
diff --git a/command/meta.go b/command/meta.go
index e95ea67ae..d55c9f8a5 100644
--- a/command/meta.go
+++ b/command/meta.go
@@ -18,6 +18,7 @@ import (
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/backend/local"
"github.com/hashicorp/terraform/command/format"
+ "github.com/hashicorp/terraform/command/webbrowser"
"github.com/hashicorp/terraform/configs/configload"
"github.com/hashicorp/terraform/helper/experiment"
"github.com/hashicorp/terraform/helper/wrappedstreams"
@@ -61,6 +62,14 @@ type Meta struct {
// the specific commands being run.
RunningInAutomation bool
+ // CLIConfigDir is the directory from which CLI configuration files were
+ // read by the caller and the directory where any changes to CLI
+ // configuration files by commands should be made.
+ //
+ // If this is empty then no configuration directory is available and
+ // commands which require one cannot proceed.
+ CLIConfigDir string
+
// PluginCacheDir, if non-empty, enables caching of downloaded plugins
// into the given directory.
PluginCacheDir string
@@ -70,6 +79,10 @@ type Meta struct {
// is not suitable, e.g. because of a read-only filesystem.
OverrideDataDir string
+ // BrowserLauncher is used by commands that need to open a URL in a
+ // web browser.
+ BrowserLauncher webbrowser.Launcher
+
// When this channel is closed, the command will be cancelled.
ShutdownCh <-chan struct{}
diff --git a/command/testdata/login-oauth-server/main.go b/command/testdata/login-oauth-server/main.go
new file mode 100644
index 000000000..70333b61a
--- /dev/null
+++ b/command/testdata/login-oauth-server/main.go
@@ -0,0 +1,71 @@
+// +build ignore
+
+// This file is a helper for those doing _manual_ testing of "terraform login"
+// and/or "terraform logout" and want to start up a test OAuth server in a
+// separate process for convenience:
+//
+// go run ./command/testdata/login-oauth-server/main.go :8080
+//
+// This is _not_ the main way to use this oauthserver package. For automated
+// test code, import it as a normal Go package instead:
+//
+// import oauthserver "github.com/hashicorp/terraform/command/testdata/login-oauth-server"
+
+package main
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "os"
+
+ oauthserver "github.com/hashicorp/terraform/command/testdata/login-oauth-server"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ fmt.Fprintln(os.Stderr, "Usage: go run ./command/testdata/login-oauth-server/main.go ")
+ os.Exit(1)
+ }
+
+ host, port, err := net.SplitHostPort(os.Args[1])
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "Invalid address: %s", err)
+ os.Exit(1)
+ }
+
+ if host == "" {
+ host = "127.0.0.1"
+ }
+ addr := fmt.Sprintf("%s:%s", host, port)
+
+ fmt.Printf("Will listen on %s...\n", addr)
+ fmt.Printf(
+ configExampleFmt,
+ fmt.Sprintf("http://%s:%s/authz", host, port),
+ fmt.Sprintf("http://%s:%s/token", host, port),
+ fmt.Sprintf("http://%s:%s/revoke", host, port),
+ )
+
+ server := &http.Server{
+ Addr: addr,
+ Handler: oauthserver.Handler,
+ }
+ err = server.ListenAndServe()
+ fmt.Fprintln(os.Stderr, err.Error())
+}
+
+const configExampleFmt = `
+host "login-test.example.com" {
+ services = {
+ "login.v1" = {
+ authz = %q
+ token = %q
+ client = "placeholder"
+ grant_types = ["code", "password"]
+ }
+ "logout.v1" = %q
+ }
+}
+
+`
diff --git a/command/testdata/login-oauth-server/oauthserver.go b/command/testdata/login-oauth-server/oauthserver.go
new file mode 100644
index 000000000..cde3477b6
--- /dev/null
+++ b/command/testdata/login-oauth-server/oauthserver.go
@@ -0,0 +1,179 @@
+// Package oauthserver is a very simplistic OAuth server used only for
+// the testing of the "terraform login" and "terraform logout" commands.
+package oauthserver
+
+import (
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "html"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// Handler is an implementation of net/http.Handler that provides a stub
+// OAuth server implementation with the following endpoints:
+//
+// /authz - authorization endpoint
+// /token - token endpoint
+// /revoke - token revocation (logout) endpoint
+//
+// The authorization endpoint returns HTML per normal OAuth conventions, but
+// it also includes an HTTP header X-Redirect-To giving the same URL that the
+// link in the HTML indicates, allowing a non-browser user-agent to traverse
+// this robotically in automated tests.
+var Handler http.Handler
+
+type handler struct{}
+
+func (h handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ switch req.URL.Path {
+ case "/authz":
+ h.serveAuthz(resp, req)
+ case "/token":
+ h.serveToken(resp, req)
+ case "/revoke":
+ h.serveRevoke(resp, req)
+ default:
+ resp.WriteHeader(404)
+ }
+}
+
+func (h handler) serveAuthz(resp http.ResponseWriter, req *http.Request) {
+ args := req.URL.Query()
+ if rt := args.Get("response_type"); rt != "code" {
+ resp.WriteHeader(400)
+ resp.Write([]byte("wrong response_type"))
+ log.Printf("/authz: incorrect response type %q", rt)
+ return
+ }
+ redirectURL, err := url.Parse(args.Get("redirect_uri"))
+ if err != nil {
+ resp.WriteHeader(400)
+ resp.Write([]byte(fmt.Sprintf("invalid redirect_uri %s: %s", args.Get("redirect_uri"), err)))
+ return
+ }
+
+ state := args.Get("state")
+ challenge := args.Get("code_challenge")
+ challengeMethod := args.Get("code_challenge_method")
+ if challengeMethod == "" {
+ challengeMethod = "plain"
+ }
+
+ // NOTE: This is not a suitable implementation for a real OAuth server
+ // because the code challenge is providing no security whatsoever. This
+ // is just a simple implementation for this stub server.
+ code := fmt.Sprintf("%s:%s", challengeMethod, challenge)
+
+ redirectQuery := redirectURL.Query()
+ redirectQuery.Set("code", code)
+ if state != "" {
+ redirectQuery.Set("state", state)
+ }
+ redirectURL.RawQuery = redirectQuery.Encode()
+
+ respBody := fmt.Sprintf(`Log In and Consent`, html.EscapeString(redirectURL.String()))
+ resp.Header().Set("Content-Type", "text/html")
+ resp.Header().Set("Content-Length", fmt.Sprintf("%d", len(respBody)))
+ resp.Header().Set("X-Redirect-To", redirectURL.String()) // For robotic clients, using webbrowser.MockLauncher
+ resp.WriteHeader(200)
+ resp.Write([]byte(respBody))
+}
+
+func (h handler) serveToken(resp http.ResponseWriter, req *http.Request) {
+ if req.Method != "POST" {
+ resp.WriteHeader(405)
+ log.Printf("/token: unsupported request method %q", req.Method)
+ return
+ }
+
+ if err := req.ParseForm(); err != nil {
+ resp.WriteHeader(500)
+ log.Printf("/token: error parsing body: %s", err)
+ return
+ }
+
+ grantType := req.Form.Get("grant_type")
+ log.Printf("/token: grant_type is %q", grantType)
+ switch grantType {
+
+ case "authorization_code":
+ code := req.Form.Get("code")
+ codeParts := strings.SplitN(code, ":", 2)
+ if len(codeParts) != 2 {
+ log.Printf("/token: invalid code %q", code)
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(400)
+ resp.Write([]byte(`{"error":"invalid_grant"}`))
+ return
+ }
+
+ codeVerifier := req.Form.Get("code_verifier")
+
+ switch codeParts[0] {
+ case "plain":
+ if codeParts[1] != codeVerifier {
+ log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], codeVerifier)
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(400)
+ resp.Write([]byte(`{"error":"invalid_grant"}`))
+ return
+ }
+ case "S256":
+ h := sha256.New()
+ h.Write([]byte(codeVerifier))
+ encVerifier := base64.URLEncoding.EncodeToString(h.Sum(nil))
+ if codeParts[1] != encVerifier {
+ log.Printf("/token: incorrect code verifier %q; want %q", codeParts[1], encVerifier)
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(400)
+ resp.Write([]byte(`{"error":"invalid_grant"}`))
+ return
+ }
+ default:
+ log.Printf("/token: unsupported challenge method %q", codeParts[0])
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(400)
+ resp.Write([]byte(`{"error":"invalid_grant"}`))
+ return
+ }
+
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(200)
+ resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`))
+ log.Println("/token: successful request")
+
+ case "password":
+ username := req.Form.Get("username")
+ password := req.Form.Get("password")
+
+ if username == "wrong" || password == "wrong" {
+ // These special "credentials" allow testing for the error case.
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(400)
+ resp.Write([]byte(`{"error":"invalid_grant"}`))
+ log.Println("/token: 'wrong' credentials")
+ return
+ }
+
+ resp.Header().Set("Content-Type", "application/json")
+ resp.WriteHeader(200)
+ resp.Write([]byte(`{"access_token":"good-token","token_type":"bearer"}`))
+ log.Println("/token: successful request")
+
+ default:
+ resp.WriteHeader(400)
+ log.Printf("/token: unsupported grant type %q", grantType)
+ }
+}
+
+func (h handler) serveRevoke(resp http.ResponseWriter, req *http.Request) {
+ resp.WriteHeader(404)
+}
+
+func init() {
+ Handler = handler{}
+}
diff --git a/command/webbrowser/mock.go b/command/webbrowser/mock.go
new file mode 100644
index 000000000..ef411ba1e
--- /dev/null
+++ b/command/webbrowser/mock.go
@@ -0,0 +1,155 @@
+package webbrowser
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "sync"
+
+ "github.com/hashicorp/terraform/httpclient"
+)
+
+// NewMockLauncher creates and returns a mock implementation of Launcher,
+// with some special behavior designed for use in unit tests.
+//
+// See the documentation of MockLauncher itself for more information.
+func NewMockLauncher(ctx context.Context) *MockLauncher {
+ client := httpclient.New()
+ return &MockLauncher{
+ Client: client,
+ Context: ctx,
+ }
+}
+
+// MockLauncher is a mock implementation of Launcher that has some special
+// behavior designed for use in unit tests.
+//
+// When OpenURL is called, MockLauncher will make an HTTP request to the given
+// URL rather than interacting with a "real" browser.
+//
+// In normal situations it will then return with no further action, but if
+// the response to the given URL is either a standard HTTP redirect response
+// or includes the custom HTTP header X-Redirect-To then MockLauncher will
+// send a follow-up request to that target URL, and continue in this manner
+// until it reaches a URL that is not a redirect. (The X-Redirect-To header
+// is there so that a server can potentially offer a normal HTML page to
+// an actual browser while also giving a next-hop hint for MockLauncher.)
+//
+// Since MockLauncher is not a full programmable user-agent implementation
+// it can't be used for testing of real-world web applications, but it can
+// be used for testing against specialized test servers that are written
+// with MockLauncher in mind and know how to drive the request flow through
+// whatever steps are required to complete the desired test.
+//
+// All of the actions taken by MockLauncher happen asynchronously in the
+// background, to simulate the concurrency of a separate web browser.
+// Test code using MockLauncher should provide a context which is cancelled
+// when the test completes, to help avoid leaking MockLaunchers.
+type MockLauncher struct {
+ // Client is the HTTP client that MockLauncher will use to make requests.
+ // By default (if you use NewMockLauncher) this is a new client created
+ // via httpclient.New, but callers may override it if they need customized
+ // behavior for a particular test.
+ //
+ // Do not use a client that is shared with any other subsystem, because
+ // MockLauncher will customize the settings of the given client.
+ Client *http.Client
+
+ // Context can be cancelled in order to abort an OpenURL call before it
+ // would naturally complete.
+ Context context.Context
+
+ // Responses is a log of all of the responses recieved from the launcher's
+ // requests, in the order requested.
+ Responses []*http.Response
+
+ // done is a waitgroup used internally to signal when the async work is
+ // complete, in order to make this mock more convenient to use in tests.
+ done sync.WaitGroup
+}
+
+var _ Launcher = (*MockLauncher)(nil)
+
+// OpenURL is the mock implementation of Launcher, which has the special
+// behavior described for type MockLauncher.
+func (l *MockLauncher) OpenURL(u string) error {
+ // We run our operation in the background because it's supposed to be
+ // behaving like a web browser running in a separate process.
+ log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) starting in the background", u)
+ l.done.Add(1)
+ go func() {
+ err := l.openURL(u)
+ if err != nil {
+ // Can't really do anything with this asynchronously, so we'll
+ // just log it so that someone debugging will be able to see it.
+ log.Printf("[ERROR] webbrowser.MockLauncher: OpenURL(%q): %s", u, err)
+ } else {
+ log.Printf("[TRACE] webbrowser.MockLauncher: OpenURL(%q) has concluded", u)
+ }
+ l.done.Done()
+ }()
+ return nil
+}
+
+func (l *MockLauncher) openURL(u string) error {
+ // We need to disable automatic redirect following so that we can implement
+ // it ourselves below, and thus be able to see the redirects in our
+ // responses log.
+ l.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ }
+
+ // We'll keep looping as long as the server keeps giving us new URLs to
+ // request.
+ for u != "" {
+ log.Printf("[DEBUG] webbrowser.MockLauncher: requesting %s", u)
+ req, err := http.NewRequest("GET", u, nil)
+ if err != nil {
+ return fmt.Errorf("failed to construct HTTP request for %s: %s", u, err)
+ }
+ resp, err := l.Client.Do(req)
+ if err != nil {
+ log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", err)
+ return fmt.Errorf("error requesting %s: %s", u, err)
+ }
+ l.Responses = append(l.Responses, resp)
+ if resp.StatusCode >= 400 {
+ log.Printf("[DEBUG] webbrowser.MockLauncher: request failed: %s", resp.Status)
+ return fmt.Errorf("error requesting %s: %s", u, resp.Status)
+ }
+ log.Printf("[DEBUG] webbrowser.MockLauncher: request succeeded: %s", resp.Status)
+
+ u = "" // unless it's a redirect, we'll stop after this
+ if location := resp.Header.Get("Location"); location != "" {
+ u = location
+ } else if redirectTo := resp.Header.Get("X-Redirect-To"); redirectTo != "" {
+ u = redirectTo
+ }
+
+ if u != "" {
+ // HTTP technically doesn't permit relative URLs in Location, but
+ // browsers tolerate it and so real-world servers do it, and thus
+ // we'll allow it here too.
+ oldURL := resp.Request.URL
+ givenURL, err := url.Parse(u)
+ if err != nil {
+ return fmt.Errorf("invalid redirect URL %s: %s", u, err)
+ }
+ u = oldURL.ResolveReference(givenURL).String()
+ log.Printf("[DEBUG] webbrowser.MockLauncher: redirected to %s", u)
+ }
+ }
+
+ log.Printf("[DEBUG] webbrowser.MockLauncher: all done")
+ return nil
+}
+
+// Wait blocks until the MockLauncher has finished its asynchronous work of
+// making HTTP requests and following redirects, at which point it will have
+// reached a request that didn't redirect anywhere and stopped iterating.
+func (l *MockLauncher) Wait() {
+ log.Printf("[TRACE] webbrowser.MockLauncher: Wait() for current work to complete")
+ l.done.Wait()
+}
diff --git a/command/webbrowser/mock_test.go b/command/webbrowser/mock_test.go
new file mode 100644
index 000000000..610f83d87
--- /dev/null
+++ b/command/webbrowser/mock_test.go
@@ -0,0 +1,95 @@
+package webbrowser
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestMockLauncher(t *testing.T) {
+ s := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+ resp.Header().Set("Content-Length", "0")
+ switch req.URL.Path {
+ case "/standard-redirect-source":
+ resp.Header().Set("Location", "/standard-redirect-target")
+ resp.WriteHeader(302)
+ case "/custom-redirect-source":
+ resp.Header().Set("X-Redirect-To", "/custom-redirect-target")
+ resp.WriteHeader(200)
+ case "/error":
+ resp.WriteHeader(500)
+ default:
+ resp.WriteHeader(200)
+ }
+ }))
+ defer s.Close()
+
+ t.Run("no redirects", func(t *testing.T) {
+ l := NewMockLauncher(context.Background())
+ err := l.OpenURL(s.URL)
+ if err != nil {
+ t.Fatal(err)
+ }
+ l.Wait() // Let the async work complete
+ if got, want := len(l.Responses), 1; got != want {
+ t.Fatalf("wrong number of responses %d; want %d", got, want)
+ }
+ if got, want := l.Responses[0].Request.URL.Path, ""; got != want {
+ t.Fatalf("wrong request URL %q; want %q", got, want)
+ }
+ })
+ t.Run("error", func(t *testing.T) {
+ l := NewMockLauncher(context.Background())
+ err := l.OpenURL(s.URL + "/error")
+ if err != nil {
+ // Th is kind of error is supposed to happen asynchronously, so we
+ // should not see it here.
+ t.Fatal(err)
+ }
+ l.Wait() // Let the async work complete
+ if got, want := len(l.Responses), 1; got != want {
+ t.Fatalf("wrong number of responses %d; want %d", got, want)
+ }
+ if got, want := l.Responses[0].Request.URL.Path, "/error"; got != want {
+ t.Fatalf("wrong request URL %q; want %q", got, want)
+ }
+ if got, want := l.Responses[0].StatusCode, 500; got != want {
+ t.Fatalf("wrong response status %d; want %d", got, want)
+ }
+ })
+ t.Run("standard redirect", func(t *testing.T) {
+ l := NewMockLauncher(context.Background())
+ err := l.OpenURL(s.URL + "/standard-redirect-source")
+ if err != nil {
+ t.Fatal(err)
+ }
+ l.Wait() // Let the async work complete
+ if got, want := len(l.Responses), 2; got != want {
+ t.Fatalf("wrong number of responses %d; want %d", got, want)
+ }
+ if got, want := l.Responses[0].Request.URL.Path, "/standard-redirect-source"; got != want {
+ t.Fatalf("wrong request 0 URL %q; want %q", got, want)
+ }
+ if got, want := l.Responses[1].Request.URL.Path, "/standard-redirect-target"; got != want {
+ t.Fatalf("wrong request 1 URL %q; want %q", got, want)
+ }
+ })
+ t.Run("custom redirect", func(t *testing.T) {
+ l := NewMockLauncher(context.Background())
+ err := l.OpenURL(s.URL + "/custom-redirect-source")
+ if err != nil {
+ t.Fatal(err)
+ }
+ l.Wait() // Let the async work complete
+ if got, want := len(l.Responses), 2; got != want {
+ t.Fatalf("wrong number of responses %d; want %d", got, want)
+ }
+ if got, want := l.Responses[0].Request.URL.Path, "/custom-redirect-source"; got != want {
+ t.Fatalf("wrong request 0 URL %q; want %q", got, want)
+ }
+ if got, want := l.Responses[1].Request.URL.Path, "/custom-redirect-target"; got != want {
+ t.Fatalf("wrong request 1 URL %q; want %q", got, want)
+ }
+ })
+}
diff --git a/command/webbrowser/native.go b/command/webbrowser/native.go
new file mode 100644
index 000000000..4e8281ce1
--- /dev/null
+++ b/command/webbrowser/native.go
@@ -0,0 +1,18 @@
+package webbrowser
+
+import (
+ "github.com/pkg/browser"
+)
+
+// NewNativeLauncher creates and returns a Launcher that will attempt to interact
+// with the browser-launching mechanisms of the operating system where the
+// program is currently running.
+func NewNativeLauncher() Launcher {
+ return nativeLauncher{}
+}
+
+type nativeLauncher struct{}
+
+func (l nativeLauncher) OpenURL(url string) error {
+ return browser.OpenURL(url)
+}
diff --git a/command/webbrowser/webbrowser.go b/command/webbrowser/webbrowser.go
new file mode 100644
index 000000000..8931ec517
--- /dev/null
+++ b/command/webbrowser/webbrowser.go
@@ -0,0 +1,19 @@
+package webbrowser
+
+// Launcher is an object that knows how to open a given URL in a new tab in
+// some suitable browser on the current system.
+//
+// Launching of browsers is a very target-platform-sensitive activity, so
+// this interface serves as an abstraction over many possible implementations
+// which can be selected based on what is appropriate for a specific situation.
+type Launcher interface {
+ // OpenURL opens the given URL in a web browser.
+ //
+ // Depending on the circumstances and on the target platform, this may or
+ // may not cause the browser to take input focus. Because of this
+ // uncertainty, any caller of this method must be sure to include some
+ // language in its UI output to let the user know that a browser tab has
+ // opened somewhere, so that they can go and find it if the focus didn't
+ // switch automatically.
+ OpenURL(url string) error
+}
diff --git a/commands.go b/commands.go
index 0a363124f..fffbb3639 100644
--- a/commands.go
+++ b/commands.go
@@ -4,12 +4,15 @@ import (
"os"
"os/signal"
+ "github.com/mitchellh/cli"
+
"github.com/hashicorp/terraform/command"
+ "github.com/hashicorp/terraform/command/cliconfig"
+ "github.com/hashicorp/terraform/command/webbrowser"
pluginDiscovery "github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/auth"
"github.com/hashicorp/terraform/svchost/disco"
- "github.com/mitchellh/cli"
)
// runningInAutomationEnvName gives the name of an environment variable that
@@ -50,6 +53,11 @@ func initCommands(config *Config, services *disco.Disco) {
services.ForceHostServices(host, hostConfig.Services)
}
+ configDir, err := cliconfig.ConfigDir()
+ if err != nil {
+ configDir = "" // No config dir available (e.g. looking up a home directory failed)
+ }
+
dataDir := os.Getenv("TF_DATA_DIR")
meta := command.Meta{
@@ -58,9 +66,11 @@ func initCommands(config *Config, services *disco.Disco) {
PluginOverrides: &PluginOverrides,
Ui: Ui,
- Services: services,
+ Services: services,
+ BrowserLauncher: webbrowser.NewNativeLauncher(),
RunningInAutomation: inAutomation,
+ CLIConfigDir: configDir,
PluginCacheDir: config.PluginCacheDir,
OverrideDataDir: dataDir,
@@ -172,6 +182,16 @@ func initCommands(config *Config, services *disco.Disco) {
}, nil
},
+ // "terraform login" is disabled until Terraform Cloud is ready to
+ // support it.
+ /*
+ "login": func() (cli.Command, error) {
+ return &command.LoginCommand{
+ Meta: meta,
+ }, nil
+ },
+ */
+
"output": func() (cli.Command, error) {
return &command.OutputCommand{
Meta: meta,
diff --git a/go.mod b/go.mod
index 8ed72f9d5..677c77cf9 100644
--- a/go.mod
+++ b/go.mod
@@ -103,6 +103,7 @@ require (
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c // indirect
+ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/pkg/errors v0.8.0
github.com/posener/complete v1.2.1
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 // indirect
diff --git a/go.sum b/go.sum
index ca6aee163..cfe54cdc3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,3 @@
-cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -22,7 +21,6 @@ github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292 h1:tuQ7w+my8a8mkwN7x2T
github.com/Unknwon/com v0.0.0-20151008135407-28b053d5a292/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
-github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE=
github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
@@ -42,7 +40,6 @@ github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0 h1:JaCC8jz0zdMLk2m+
github.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=
github.com/apparentlymart/go-cidr v1.0.0 h1:lGDvXx8Lv9QHjrAVP7jyzleG4F9+FkRhJcEsDFxeb8w=
github.com/apparentlymart/go-cidr v1.0.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
-github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I=
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
@@ -56,7 +53,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=
-github.com/aws/aws-sdk-go v1.16.36 h1:POeH34ZME++pr7GBGh+ZO6Y5kOwSMQpqp5BGUgooJ6k=
github.com/aws/aws-sdk-go v1.16.36/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.22.0 h1:e88V6+dSEyBibUy0ekOydtTfNWzqG3hrtCR8SF6UqqY=
github.com/aws/aws-sdk-go v1.22.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@@ -124,23 +120,19 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE=
github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -208,13 +200,11 @@ github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2Y
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-tfe v0.3.16 h1:GS2yv580p0co4j3FBVaC6Zahd9mxdCGehhJ0qqzFMH0=
github.com/hashicorp/go-tfe v0.3.16/go.mod h1:SuPHR+OcxvzBZNye7nGPfwZTEyd3rWPfLVbCgyZPezM=
-github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -251,7 +241,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
@@ -265,7 +254,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
@@ -279,12 +267,10 @@ github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9 h1:SmVbOZFWAly
github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=
github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b h1:/1RFh2SLCJ+tEnT73+Fh5R2AO89sQqs8ba7o+hx1G0Y=
github.com/masterzen/winrm v0.0.0-20190223112901-5e5c9a7fe54b/go.mod h1:wr1VqkwW0AB5JS0QLy5GpVMS9E3VtRoSYXUYyVk46KY=
-github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -301,13 +287,11 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
-github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb h1:GRiLv4rgyqjqzxbhJke65IYUf4NCOOvrPOJbV/sPxkM=
github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM=
-github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -340,18 +324,18 @@ github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58 h1:m3CEgv
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
+github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI=
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -377,9 +361,7 @@ github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -405,7 +387,6 @@ github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 h1:MPPkRncZLN9Kh4M
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 h1:Jpn2j6wHkC9wJv5iMfJhKqrZJx3TahFx+7sbZ7zQdxs=
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
-github.com/zclconf/go-cty v1.0.0 h1:EWtv3gKe2wPLIB9hQRQJa7k/059oIfAqcEkCNnaVckk=
github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.0.1-0.20190708163926-19588f92a98f h1:sq2p8SN6ji66CFEQFIWLlD/gFmGtr5hBrOzv5nLlGfA=
github.com/zclconf/go-cty v1.0.1-0.20190708163926-19588f92a98f/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
@@ -422,13 +403,9 @@ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 h1:NwxKRvbkH5MsNkvOtPZi3/3kmI8CAzs3mtv+GLQMkNo=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f h1:qWFY9ZxP3tfI37wYIs/MnIAqK0vlXp1xnYEa5HxFSSY=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
@@ -450,28 +427,23 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190502183928-7f726cade0ab h1:9RfW3ktsOZxgo9YNbBAjq1FWzc/igwEcUzZz8IXgSbk=
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -479,30 +451,23 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0 h1:bzeyCHgoAyjZjAhvTpks+qM7sdlh4cCSitmXeCEO3B4=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w=
golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -522,13 +487,11 @@ google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEn
google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -536,7 +499,6 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
diff --git a/main.go b/main.go
index 5412a5b72..3cc867801 100644
--- a/main.go
+++ b/main.go
@@ -106,7 +106,10 @@ func init() {
OutputPrefix: OutputPrefix,
InfoPrefix: OutputPrefix,
ErrorPrefix: ErrorPrefix,
- Ui: &cli.BasicUi{Writer: os.Stdout},
+ Ui: &cli.BasicUi{
+ Writer: os.Stdout,
+ Reader: os.Stdin,
+ },
}
}
diff --git a/vendor/github.com/pkg/browser/LICENSE b/vendor/github.com/pkg/browser/LICENSE
new file mode 100644
index 000000000..65f78fb62
--- /dev/null
+++ b/vendor/github.com/pkg/browser/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2014, Dave Cheney
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/pkg/browser/README.md b/vendor/github.com/pkg/browser/README.md
new file mode 100644
index 000000000..72b1976e3
--- /dev/null
+++ b/vendor/github.com/pkg/browser/README.md
@@ -0,0 +1,55 @@
+
+# browser
+ import "github.com/pkg/browser"
+
+Package browser provides helpers to open files, readers, and urls in a browser window.
+
+The choice of which browser is started is entirely client dependant.
+
+
+
+
+
+## Variables
+``` go
+var Stderr io.Writer = os.Stderr
+```
+Stderr is the io.Writer to which executed commands write standard error.
+
+``` go
+var Stdout io.Writer = os.Stdout
+```
+Stdout is the io.Writer to which executed commands write standard output.
+
+
+## func OpenFile
+``` go
+func OpenFile(path string) error
+```
+OpenFile opens new browser window for the file path.
+
+
+## func OpenReader
+``` go
+func OpenReader(r io.Reader) error
+```
+OpenReader consumes the contents of r and presents the
+results in a new browser window.
+
+
+## func OpenURL
+``` go
+func OpenURL(url string) error
+```
+OpenURL opens a new browser window pointing to url.
+
+
+
+
+
+
+
+
+
+- - -
+Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
diff --git a/vendor/github.com/pkg/browser/browser.go b/vendor/github.com/pkg/browser/browser.go
new file mode 100644
index 000000000..3e5969064
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser.go
@@ -0,0 +1,63 @@
+// Package browser provides helpers to open files, readers, and urls in a browser window.
+//
+// The choice of which browser is started is entirely client dependant.
+package browser
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// Stdout is the io.Writer to which executed commands write standard output.
+var Stdout io.Writer = os.Stdout
+
+// Stderr is the io.Writer to which executed commands write standard error.
+var Stderr io.Writer = os.Stderr
+
+// OpenFile opens new browser window for the file path.
+func OpenFile(path string) error {
+ path, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+ return OpenURL("file://" + path)
+}
+
+// OpenReader consumes the contents of r and presents the
+// results in a new browser window.
+func OpenReader(r io.Reader) error {
+ f, err := ioutil.TempFile("", "browser")
+ if err != nil {
+ return fmt.Errorf("browser: could not create temporary file: %v", err)
+ }
+ if _, err := io.Copy(f, r); err != nil {
+ f.Close()
+ return fmt.Errorf("browser: caching temporary file failed: %v", err)
+ }
+ if err := f.Close(); err != nil {
+ return fmt.Errorf("browser: caching temporary file failed: %v", err)
+ }
+ oldname := f.Name()
+ newname := oldname + ".html"
+ if err := os.Rename(oldname, newname); err != nil {
+ return fmt.Errorf("browser: renaming temporary file failed: %v", err)
+ }
+ return OpenFile(newname)
+}
+
+// OpenURL opens a new browser window pointing to url.
+func OpenURL(url string) error {
+ return openBrowser(url)
+}
+
+func runCmd(prog string, args ...string) error {
+ cmd := exec.Command(prog, args...)
+ cmd.Stdout = Stdout
+ cmd.Stderr = Stderr
+ setFlags(cmd)
+ return cmd.Run()
+}
diff --git a/vendor/github.com/pkg/browser/browser_darwin.go b/vendor/github.com/pkg/browser/browser_darwin.go
new file mode 100644
index 000000000..6dff0403c
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser_darwin.go
@@ -0,0 +1,9 @@
+package browser
+
+import "os/exec"
+
+func openBrowser(url string) error {
+ return runCmd("open", url)
+}
+
+func setFlags(cmd *exec.Cmd) {}
diff --git a/vendor/github.com/pkg/browser/browser_linux.go b/vendor/github.com/pkg/browser/browser_linux.go
new file mode 100644
index 000000000..656c693ba
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser_linux.go
@@ -0,0 +1,9 @@
+package browser
+
+import "os/exec"
+
+func openBrowser(url string) error {
+ return runCmd("xdg-open", url)
+}
+
+func setFlags(cmd *exec.Cmd) {}
diff --git a/vendor/github.com/pkg/browser/browser_openbsd.go b/vendor/github.com/pkg/browser/browser_openbsd.go
new file mode 100644
index 000000000..8cc0a7f53
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser_openbsd.go
@@ -0,0 +1,16 @@
+package browser
+
+import (
+ "errors"
+ "os/exec"
+)
+
+func openBrowser(url string) error {
+ err := runCmd("xdg-open", url)
+ if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound {
+ return errors.New("xdg-open: command not found - install xdg-utils from ports(8)")
+ }
+ return err
+}
+
+func setFlags(cmd *exec.Cmd) {}
diff --git a/vendor/github.com/pkg/browser/browser_unsupported.go b/vendor/github.com/pkg/browser/browser_unsupported.go
new file mode 100644
index 000000000..0e1e530c5
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser_unsupported.go
@@ -0,0 +1,15 @@
+// +build !linux,!windows,!darwin,!openbsd
+
+package browser
+
+import (
+ "fmt"
+ "os/exec"
+ "runtime"
+)
+
+func openBrowser(url string) error {
+ return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS)
+}
+
+func setFlags(cmd *exec.Cmd) {}
diff --git a/vendor/github.com/pkg/browser/browser_windows.go b/vendor/github.com/pkg/browser/browser_windows.go
new file mode 100644
index 000000000..a964c7b91
--- /dev/null
+++ b/vendor/github.com/pkg/browser/browser_windows.go
@@ -0,0 +1,16 @@
+package browser
+
+import (
+ "os/exec"
+ "strings"
+ "syscall"
+)
+
+func openBrowser(url string) error {
+ r := strings.NewReplacer("&", "^&")
+ return runCmd("cmd", "/c", "start", r.Replace(url))
+}
+
+func setFlags(cmd *exec.Cmd) {
+ cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index cda61eb84..8dac64c8e 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -437,6 +437,8 @@ github.com/nu7hatch/gouuid
github.com/oklog/run
# github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58
github.com/packer-community/winrmcp/winrmcp
+# github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
+github.com/pkg/browser
# github.com/pkg/errors v0.8.0
github.com/pkg/errors
# github.com/posener/complete v1.2.1