From 106b9ad6964e95fd28b96a04d90138dc87a2dbe7 Mon Sep 17 00:00:00 2001 From: Francisco Gray Date: Sat, 31 Jan 2026 16:44:06 -0800 Subject: [PATCH] feat: adds support for reading terraform-cloud tokens from 1Password --- plugins/terraform/api_token.go | 107 ++++++++++++++++++++++++++++ plugins/terraform/api_token_test.go | 55 ++++++++++++++ plugins/terraform/plugin.go | 3 + 3 files changed, 165 insertions(+) create mode 100644 plugins/terraform/api_token.go create mode 100644 plugins/terraform/api_token_test.go diff --git a/plugins/terraform/api_token.go b/plugins/terraform/api_token.go new file mode 100644 index 000000000..e2e4264c6 --- /dev/null +++ b/plugins/terraform/api_token.go @@ -0,0 +1,107 @@ +package terraform + +import ( + "context" + "fmt" + "strings" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func APIToken() schema.CredentialType { + return schema.CredentialType{ + Name: credname.APIToken, + DocsURL: sdk.URL("https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/users#tokens"), + ManagementURL: sdk.URL("https://app.terraform.io/app/settings/tokens"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Token, + MarkdownDescription: "Token used to authenticate to Terraform Cloud.", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 90, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + }, + }, + }, + { + Name: fieldname.Host, + MarkdownDescription: "Hostname used to authenticate to which Terraform Cloud instance (default: app.terraform.io).", + }, + }, + DefaultProvisioner: &TerraformProvisioner{}, + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + TryTerraformConfigFile(), + )} +} + +type TerraformProvisioner struct { + sdk.Provisioner + + Schema map[string]sdk.FieldName +} + +func (p *TerraformProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) { + if hostValue, ok := in.ItemFields[fieldname.Host]; ok { + if tokenValue, ok := in.ItemFields[fieldname.Token]; ok { + envVarName := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(hostValue, ".", "_")) + out.AddEnvVar(envVarName, tokenValue) + } + } +} + +func (p *TerraformProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) { + // Nothing to do here: environment variables get wiped automatically when the process exits. +} + +func (p TerraformProvisioner) Description() string { + var envVarNames []string + for envVarName := range p.Schema { + envVarNames = append(envVarNames, envVarName) + } + + return fmt.Sprintf("Provision environment variables: %s", strings.Join(envVarNames, ", ")) +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "TF_TOKEN_app_terraform_io": fieldname.Token, +} + +func TryTerraformConfigFile() sdk.Importer { + return importer.TryFile("~/.terraform.d/credentials.tfrc.json", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + var config Config + if err := contents.ToJSON(&config); err != nil { + out.AddError(err) + return + } + + if len(config.Credentials) == 0 { + return + } + + for host, cred := range config.Credentials { + out.AddCandidate(sdk.ImportCandidate{ + Fields: map[sdk.FieldName]string{ + fieldname.Host: host, + fieldname.Token: cred.Token, + }, + }) + } + }) +} + +type Config struct { + Credentials map[string]Credential `json:"credentials"` +} + +type Credential struct { + Token string `json:"token"` +} diff --git a/plugins/terraform/api_token_test.go b/plugins/terraform/api_token_test.go new file mode 100644 index 000000000..4f3bf6e78 --- /dev/null +++ b/plugins/terraform/api_token_test.go @@ -0,0 +1,55 @@ +package terraform + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestAPITokenProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, APIToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ // TODO: Check if this is correct + fieldname.Token: "tlMlxpJCrwU66MOY23rPC5v8ZXe7ZjCnC5j2DaztjKdCJi20N7kTI6v86YtjOdG5t0VWYNOSnAAjvMcLsoJEXAMPLE", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "TF_TOKEN_app_terraform_io": "tlMlxpJCrwU66MOY23rPC5v8ZXe7ZjCnC5j2DaztjKdCJi20N7kTI6v86YtjOdG5t0VWYNOSnAAjvMcLsoJEXAMPLE", + }, + }, + }, + }) +} + +func TestAPITokenImporter(t *testing.T) { + plugintest.TestImporter(t, APIToken().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ // TODO: Check if this is correct + "TF_TOKEN_app_terraform_io": "tlMlxpJCrwU66MOY23rPC5v8ZXe7ZjCnC5j2DaztjKdCJi20N7kTI6v86YtjOdG5t0VWYNOSnAAjvMcLsoJEXAMPLE", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Token: "tlMlxpJCrwU66MOY23rPC5v8ZXe7ZjCnC5j2DaztjKdCJi20N7kTI6v86YtjOdG5t0VWYNOSnAAjvMcLsoJEXAMPLE", + }, + }, + }, + }, + // TODO: If you implemented a config file importer, add a test file example in terraform-cloud/test-fixtures + // and fill the necessary details in the test template below. + "config file": { + Files: map[string]string{ + // "~/path/to/config.yml": plugintest.LoadFixture(t, "config.yml"), + }, + ExpectedCandidates: []sdk.ImportCandidate{ + // { + // Fields: map[sdk.FieldName]string{ + // fieldname.Token: "tlMlxpJCrwU66MOY23rPC5v8ZXe7ZjCnC5j2DaztjKdCJi20N7kTI6v86YtjOdG5t0VWYNOSnAAjvMcLsoJEXAMPLE", + // }, + // }, + }, + }, + }) +} diff --git a/plugins/terraform/plugin.go b/plugins/terraform/plugin.go index 404fb9b94..b9e05266b 100644 --- a/plugins/terraform/plugin.go +++ b/plugins/terraform/plugin.go @@ -12,6 +12,9 @@ func New() schema.Plugin { Name: "Terraform", Homepage: sdk.URL("https://www.terraform.io"), }, + Credentials: []schema.CredentialType{ + APIToken(), + }, Executables: []schema.Executable{ TerraformCLI(), },