github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/logout.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	svchost "github.com/hashicorp/terraform-svchost"
     9  	"github.com/hashicorp/terraform/command/cliconfig"
    10  	"github.com/hashicorp/terraform/tfdiags"
    11  )
    12  
    13  // LogoutCommand is a Command implementation which removes stored credentials
    14  // for a remote service host.
    15  type LogoutCommand struct {
    16  	Meta
    17  }
    18  
    19  // Run implements cli.Command.
    20  func (c *LogoutCommand) Run(args []string) int {
    21  	args, err := c.Meta.process(args, false)
    22  	if err != nil {
    23  		return 1
    24  	}
    25  
    26  	cmdFlags := c.Meta.defaultFlagSet("logout")
    27  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    28  	if err := cmdFlags.Parse(args); err != nil {
    29  		return 1
    30  	}
    31  
    32  	args = cmdFlags.Args()
    33  	if len(args) > 1 {
    34  		c.Ui.Error(
    35  			"The logout command expects at most one argument: the host to log out of.")
    36  		cmdFlags.Usage()
    37  		return 1
    38  	}
    39  
    40  	var diags tfdiags.Diagnostics
    41  
    42  	givenHostname := "app.terraform.io"
    43  	if len(args) != 0 {
    44  		givenHostname = args[0]
    45  	}
    46  
    47  	hostname, err := svchost.ForComparison(givenHostname)
    48  	if err != nil {
    49  		diags = diags.Append(tfdiags.Sourceless(
    50  			tfdiags.Error,
    51  			"Invalid hostname",
    52  			fmt.Sprintf("The given hostname %q is not valid: %s.", givenHostname, err.Error()),
    53  		))
    54  		c.showDiagnostics(diags)
    55  		return 1
    56  	}
    57  
    58  	// From now on, since we've validated the given hostname, we should use
    59  	// dispHostname in the UI to ensure we're presenting it in the canonical
    60  	// form, in case that helps users with debugging when things aren't
    61  	// working as expected. (Perhaps the normalization is part of the cause.)
    62  	dispHostname := hostname.ForDisplay()
    63  
    64  	creds := c.Services.CredentialsSource().(*cliconfig.CredentialsSource)
    65  	filename, _ := creds.CredentialsFilePath()
    66  	credsCtx := &loginCredentialsContext{
    67  		Location:      creds.HostCredentialsLocation(hostname),
    68  		LocalFilename: filename, // empty in the very unlikely event that we can't select a config directory for this user
    69  		HelperType:    creds.CredentialsHelperType(),
    70  	}
    71  
    72  	if credsCtx.Location == cliconfig.CredentialsInOtherFile {
    73  		diags = diags.Append(tfdiags.Sourceless(
    74  			tfdiags.Error,
    75  			fmt.Sprintf("Credentials for %s are manually configured", dispHostname),
    76  			"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.",
    77  		))
    78  	}
    79  
    80  	if diags.HasErrors() {
    81  		c.showDiagnostics(diags)
    82  		return 1
    83  	}
    84  
    85  	// credsCtx might not be set if we're using a mock credentials source
    86  	// in a test, but it should always be set in normal use.
    87  	if credsCtx != nil {
    88  		switch credsCtx.Location {
    89  		case cliconfig.CredentialsNotAvailable:
    90  			c.Ui.Output(fmt.Sprintf("No credentials for %s are stored.\n", dispHostname))
    91  			return 0
    92  		case cliconfig.CredentialsViaHelper:
    93  			c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the configured\n%q credentials helper.\n", dispHostname, credsCtx.HelperType))
    94  		case cliconfig.CredentialsInPrimaryFile:
    95  			c.Ui.Output(fmt.Sprintf("Removing the stored credentials for %s from the following file:\n    %s\n", dispHostname, credsCtx.LocalFilename))
    96  		}
    97  	}
    98  
    99  	err = creds.ForgetForHost(hostname)
   100  	if err != nil {
   101  		diags = diags.Append(tfdiags.Sourceless(
   102  			tfdiags.Error,
   103  			"Failed to remove API token",
   104  			fmt.Sprintf("Unable to remove stored API token: %s", err),
   105  		))
   106  	}
   107  
   108  	c.showDiagnostics(diags)
   109  	if diags.HasErrors() {
   110  		return 1
   111  	}
   112  
   113  	c.Ui.Output(
   114  		fmt.Sprintf(
   115  			c.Colorize().Color(strings.TrimSpace(`
   116  [green][bold]Success![reset] [bold]Terraform has removed the stored API token for %s.[reset]
   117  `)),
   118  			dispHostname,
   119  		) + "\n",
   120  	)
   121  
   122  	return 0
   123  }
   124  
   125  // Help implements cli.Command.
   126  func (c *LogoutCommand) Help() string {
   127  	defaultFile := c.defaultOutputFile()
   128  	if defaultFile == "" {
   129  		// Because this is just for the help message and it's very unlikely
   130  		// that a user wouldn't have a functioning home directory anyway,
   131  		// we'll just use a placeholder here. The real command has some
   132  		// more complex behavior for this case. This result is not correct
   133  		// on all platforms, but given how unlikely we are to hit this case
   134  		// that seems okay.
   135  		defaultFile = "~/.terraform/credentials.tfrc.json"
   136  	}
   137  
   138  	helpText := `
   139  Usage: terraform logout [hostname]
   140  
   141    Removes locally-stored credentials for specified hostname.
   142  
   143    Note: the API token is only removed from local storage, not destroyed on the
   144    remote server, so it will remain valid until manually revoked.
   145  
   146    If no hostname is provided, the default hostname is app.terraform.io.
   147        %s
   148  `
   149  	return strings.TrimSpace(helpText)
   150  }
   151  
   152  // Synopsis implements cli.Command.
   153  func (c *LogoutCommand) Synopsis() string {
   154  	return "Remove locally-stored credentials for a remote host"
   155  }
   156  
   157  func (c *LogoutCommand) defaultOutputFile() string {
   158  	if c.CLIConfigDir == "" {
   159  		return "" // no default available
   160  	}
   161  	return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json")
   162  }