| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "fmt" | |
| "path/filepath" | |
| "strings" | |
| svchost "github.com/hashicorp/terraform-svchost" | |
| "github.com/hashicorp/terraform/internal/command/cliconfig" | |
| "github.com/hashicorp/terraform/internal/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 = c.Meta.process(args) | |
| 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().(*cliconfig.CredentialsSource) | |
| filename, _ := creds.CredentialsFilePath() | |
| credsCtx := &loginCredentialsContext{ | |
| Location: creds.HostCredentialsLocation(hostname), | |
| LocalFilename: filename, // empty in the very unlikely event that we can't select a config directory for this user | |
| HelperType: creds.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 | |
| } | |
| switch credsCtx.Location { | |
| case cliconfig.CredentialsNotAvailable: | |
| c.Ui.Output(fmt.Sprintf("No credentials for %s are stored.\n", dispHostname)) | |
| return 0 | |
| 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: | |
| 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 [global options] 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") | |
| } |