command/logout: Add terraform logout command
Use terraform logout to remove stored credentials for a remote service host.
This commit is contained in:
parent
1b1327ce45
commit
081f02971d
|
@ -0,0 +1,168 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
"github.com/hashicorp/terraform/command/cliconfig"
|
||||||
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogoutCommand is a Command implementation which removes stored credentials
|
||||||
|
// for a remote service host.
|
||||||
|
type LogoutCommand struct {
|
||||||
|
Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run implements cli.Command.
|
||||||
|
func (c *LogoutCommand) Run(args []string) int {
|
||||||
|
args, err := c.Meta.process(args, false)
|
||||||
|
if err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdFlags := c.Meta.defaultFlagSet("logout")
|
||||||
|
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 logout command expects at most one argument: the host to log out of.")
|
||||||
|
cmdFlags.Usage()
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
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 helps users with debugging when things aren't
|
||||||
|
// working as expected. (Perhaps the normalization is part of the cause.)
|
||||||
|
dispHostname := hostname.ForDisplay()
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if credsCtx.Location == cliconfig.CredentialsInOtherFile {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
fmt.Sprintf("Credentials for %s are manually configured", dispHostname),
|
||||||
|
"The \"terraform logout\" command cannot log out because credentials for this host are manually configured in a CLI configuration file.\n\nTo log out, revoke the existing credentials and remove that block from the CLI configuration.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if diags.HasErrors() {
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("Removing the stored credentials for %s from the configured\n%q credentials helper.\n", dispHostname, credsCtx.HelperType))
|
||||||
|
case cliconfig.CredentialsInPrimaryFile, cliconfig.CredentialsNotAvailable:
|
||||||
|
c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the following file:\n %s\n", dispHostname, credsCtx.LocalFilename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = creds.ForgetForHost(hostname)
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
|
tfdiags.Error,
|
||||||
|
"Failed to remove API token",
|
||||||
|
fmt.Sprintf("Unable to remove stored API token: %s", err),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.showDiagnostics(diags)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Ui.Output(
|
||||||
|
fmt.Sprintf(
|
||||||
|
c.Colorize().Color(strings.TrimSpace(`
|
||||||
|
[green][bold]Success![reset] [bold]Terraform has removed the stored API token for %s.[reset]
|
||||||
|
`)),
|
||||||
|
dispHostname,
|
||||||
|
) + "\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Help implements cli.Command.
|
||||||
|
func (c *LogoutCommand) 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 := `
|
||||||
|
Usage: terraform logout [hostname]
|
||||||
|
|
||||||
|
Removes locally-stored credentials for specified hostname.
|
||||||
|
|
||||||
|
Note: the API token is only removed from local storage, not destroyed on the
|
||||||
|
remote server, so it will remain valid until manually revoked.
|
||||||
|
|
||||||
|
If no hostname is provided, the default hostname is app.terraform.io.
|
||||||
|
%s
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synopsis implements cli.Command.
|
||||||
|
func (c *LogoutCommand) Synopsis() string {
|
||||||
|
return "Remove locally-stored credentials for a remote host"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LogoutCommand) defaultOutputFile() string {
|
||||||
|
if c.CLIConfigDir == "" {
|
||||||
|
return "" // no default available
|
||||||
|
}
|
||||||
|
return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json")
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
|
||||||
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
svcauth "github.com/hashicorp/terraform-svchost/auth"
|
||||||
|
"github.com/hashicorp/terraform-svchost/disco"
|
||||||
|
"github.com/hashicorp/terraform/command/cliconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogout(t *testing.T) {
|
||||||
|
workDir, err := ioutil.TempDir("", "terraform-test-command-logout")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cannot create temporary directory: %s", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(workDir)
|
||||||
|
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
credsSrc := cliconfig.EmptyCredentialsSourceForTests(filepath.Join(workDir, "credentials.tfrc.json"))
|
||||||
|
|
||||||
|
c := &LogoutCommand{
|
||||||
|
Meta: Meta{
|
||||||
|
Ui: ui,
|
||||||
|
Services: disco.NewWithCredentialsSource(credsSrc),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
// Hostname to associate a pre-stored token
|
||||||
|
hostname string
|
||||||
|
// Command-line arguments
|
||||||
|
args []string
|
||||||
|
// true iff the token at hostname should be removed by the command
|
||||||
|
shouldRemove bool
|
||||||
|
}{
|
||||||
|
// If no command-line arguments given, should remove app.terraform.io token
|
||||||
|
{"app.terraform.io", []string{}, true},
|
||||||
|
|
||||||
|
// Can still specify app.terraform.io explicitly
|
||||||
|
{"app.terraform.io", []string{"app.terraform.io"}, true},
|
||||||
|
|
||||||
|
// Can remove tokens for other hostnames
|
||||||
|
{"tfe.example.com", []string{"tfe.example.com"}, true},
|
||||||
|
|
||||||
|
// Logout does not remove tokens for other hostnames
|
||||||
|
{"tfe.example.com", []string{"other-tfe.acme.com"}, false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
host := svchost.Hostname(tc.hostname)
|
||||||
|
token := svcauth.HostCredentialsToken("some-token")
|
||||||
|
err = credsSrc.StoreForHost(host, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error storing credentials: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := c.Run(tc.args)
|
||||||
|
if status != 0 {
|
||||||
|
t.Fatalf("unexpected error code %d\nstderr:\n%s", status, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := credsSrc.ForHost(host)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to retrieve credentials: %s", err)
|
||||||
|
}
|
||||||
|
if tc.shouldRemove {
|
||||||
|
if creds != nil {
|
||||||
|
t.Errorf("wrong token %q; should have no token", creds.Token())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if got, want := creds.Token(), "some-token"; got != want {
|
||||||
|
t.Errorf("wrong token %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -190,6 +190,12 @@ func initCommands(config *cliconfig.Config, services *disco.Disco, providerSrc g
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"logout": func() (cli.Command, error) {
|
||||||
|
return &command.LogoutCommand{
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"output": func() (cli.Command, error) {
|
"output": func() (cli.Command, error) {
|
||||||
return &command.OutputCommand{
|
return &command.OutputCommand{
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Command: logout"
|
||||||
|
sidebar_current: "docs-commands-logout"
|
||||||
|
description: |-
|
||||||
|
The terraform logout command is used to remove credentials stored by terraform login.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Command: logout
|
||||||
|
|
||||||
|
The `terraform logout` command is used to remove credentials stored by
|
||||||
|
`terraform login`. These credentials are API tokens for Terraform Cloud,
|
||||||
|
Terraform Enterprise, or any other host that offers Terraform services.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `terraform logout [hostname]`
|
||||||
|
|
||||||
|
If you don't provide an explicit hostname, Terraform will assume you want to
|
||||||
|
log out of Terraform Cloud at `app.terraform.io`.
|
||||||
|
|
||||||
|
-> **Note:** the API token is only removed from local storage, not destroyed on
|
||||||
|
the remote server, so it will remain valid until manually revoked.
|
||||||
|
|
||||||
|
## Credentials Storage
|
||||||
|
|
||||||
|
By default, Terraform will remove the token stored in plain text in a local CLI
|
||||||
|
configuration file called `credentials.tfrc.json`. If you have configured a
|
||||||
|
[credentials helper program](cli-config.html#credentials-helpers), Terraform
|
||||||
|
will use the helper's `forget` command to remove it.
|
|
@ -190,6 +190,10 @@
|
||||||
<a href="/docs/commands/login.html">login</a>
|
<a href="/docs/commands/login.html">login</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-commands-logout") %>>
|
||||||
|
<a href="/docs/commands/logout.html">logout</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-commands-output") %>>
|
<li<%= sidebar_current("docs-commands-output") %>>
|
||||||
<a href="/docs/commands/output.html">output</a>
|
<a href="/docs/commands/output.html">output</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue