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
|
||||
},
|
||||
|
||||
"logout": func() (cli.Command, error) {
|
||||
return &command.LogoutCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"output": func() (cli.Command, error) {
|
||||
return &command.OutputCommand{
|
||||
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>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-logout") %>>
|
||||
<a href="/docs/commands/logout.html">logout</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-output") %>>
|
||||
<a href="/docs/commands/output.html">output</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue