Skip to content

Commit

Permalink
get credentials from user's terraform config directory
Browse files Browse the repository at this point in the history
  • Loading branch information
briskt committed Sep 16, 2024
1 parent 9f350ed commit e1fae67
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 33 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
# Terraform Cloud Ops Tool

This application can be helpful in making copies/clones of a workspace and bringing its variables over
to the new one. It can also be used for listing or updating workspace attributes and listing or
modifying variables in workspaces.

## Required ENV vars
- `ATLAS_TOKEN` - Must be set as an environment variable. Get this by going to
https://app.terraform.io/app/settings/tokens and generating a new token.
- `ATLAS_TOKEN_DESTINATION` - Only necessary if cloning to a new organization in TF Cloud.

## Optional ENV vars
- `TFC_OPS_DEBUG` - Set to `true` to enable debug output

## Installation

There are three ways to download/install this script:

1. Download a pre-built binary for your operating system from the [Releases](https://github.com/silinternational/tfc-ops/releases) page.
2. If you're a Go developer you can install it by running `go get -u https://github.com/silinternational/tfc-ops.git`
3. If you're a Go developer and want to modify the source before running, clone this repo and run with `go run main.go ...`

## Configuration

To provide access to HCP Terraform (Terraform Cloud) run the `terraform login` command and follow the prompts. This
will store a short-lived token on your computer. tfc-ops uses this token to make API calls to HCP Terraform.

## Environment vars
- `TFC_OPS_DEBUG` - Set to `true` to enable debug output
- `ATLAS_TOKEN` - An HCP Terraform token can be set as an environment variable. Get this by going to
https://app.terraform.io/app/settings/tokens and generating a new token. The recommended alternative is to use
the `terraform login` command to request a short-lived token.
- `ATLAS_TOKEN_DESTINATION` - Only necessary if cloning to a new organization in TF Cloud.

## Cloning a TF Cloud Workspace
Examples.

Expand Down
82 changes: 76 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
package cmd

import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -59,12 +63,7 @@ func init() {
}

func initRoot(cmd *cobra.Command, args []string) {
// Get Tokens from env vars
atlasToken := os.Getenv("ATLAS_TOKEN")
if atlasToken == "" {
errLog.Fatalln("Error: Environment variable for ATLAS_TOKEN is required to execute plan and migration")
}
lib.SetToken(atlasToken)
getToken()

debugStr := os.Getenv("TFC_OPS_DEBUG")
if debugStr == "TRUE" || debugStr == "true" {
Expand All @@ -76,6 +75,77 @@ func initRoot(cmd *cobra.Command, args []string) {
}
}

type Credentials struct {
Credentials struct {
AppTerraformIo struct {
Token string `json:"token"`
} `json:"app.terraform.io"`
} `json:"credentials"`
}

func getToken() {
credentials, err := readTerraformCredentials()
if err != nil {
errLog.Fatalln("failed to get Terraform credentials:", err)
}

if credentials != nil {
token := credentials.Credentials.AppTerraformIo.Token
if token != "" {
lib.SetToken(token)
return
}
}

// fall back to using ATLAS_TOKEN environment variable
atlasToken := os.Getenv("ATLAS_TOKEN")
if atlasToken != "" {
lib.SetToken(atlasToken)
return
}

errLog.Fatalln("no credentials found, use 'terraform login' to create a token")
}

func readTerraformCredentials() (*Credentials, error) {
var configDir string

if runtime.GOOS == "windows" {
var err error
configDir, err = os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("unable to get the config directory: %v", err)
}
} else {
var err error
configDir, err = os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("unable to get the home directory: %v", err)
}
}

credentialsPath := filepath.Join(configDir, ".terraform.d", "ceredentials.tfrc.json")
fmt.Println(credentialsPath)
if _, err := os.Stat(credentialsPath); errors.Is(err, os.ErrNotExist) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("error checking file existence: %v", err)
}

fileContents, err := os.ReadFile(credentialsPath)
if err != nil {
return nil, fmt.Errorf("unable to read credentials file: %v", err)
}

var creds Credentials
err = json.Unmarshal(fileContents, &creds)
if err != nil {
return nil, fmt.Errorf("unable to parse JSON: %v", err)
}

return &creds, nil
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
Expand Down
4 changes: 2 additions & 2 deletions cmd/workspacesClone.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ func runClone(cfg cloner.CloneConfig) {

cfg.AtlasTokenDestination = os.Getenv("ATLAS_TOKEN_DESTINATION")
if cfg.AtlasTokenDestination == "" {
cfg.AtlasTokenDestination = os.Getenv("ATLAS_TOKEN")
fmt.Print("Info: ATLAS_TOKEN_DESTINATION is not set, using ATLAS_TOKEN for destination account.\n\n")
cfg.AtlasTokenDestination = cloner.GetToken()
fmt.Print("Info: ATLAS_TOKEN_DESTINATION is not set, using primary credential for destination account.\n\n")
}

fmt.Printf("clone called using %s, %s, %s, copyState: %t, copyVariables: %t, "+
Expand Down
24 changes: 7 additions & 17 deletions lib/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ type CloneConfig struct {
SourceWorkspace string
NewWorkspace string
NewVCSTokenID string
AtlasToken string
AtlasTokenDestination string
CopyState bool
CopyVariables bool
Expand Down Expand Up @@ -671,14 +670,12 @@ func CreateWorkspace2(oc OpsConfig, vcsTokenID string) (Workspace, error) {
//
// NOTE: This procedure can be used to copy/migrate a workspace's state to a new one.
// (see the -backend-config mention below and the backend.tf file in this repo)
func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
func RunTFInit(oc OpsConfig, tfTokenDestination string) error {
var tfInit string
var err error
var osCmd *exec.Cmd
var stderr bytes.Buffer

tokenEnv := "ATLAS_TOKEN"

stateFile := ".terraform"

// Remove previous state file, if it exists
Expand All @@ -690,10 +687,6 @@ func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
}
}

if err := os.Setenv(tokenEnv, tfToken); err != nil {
return fmt.Errorf("Error setting %s environment variable to source value: %s", tokenEnv, err)
}

tfInit = fmt.Sprintf(`-backend-config=name=%s/%s`, oc.SourceOrg, oc.SourceName)

osCmd = exec.Command("terraform", "init", tfInit)
Expand All @@ -706,9 +699,7 @@ func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
return err
}

if err := os.Setenv(tokenEnv, tfTokenDestination); err != nil {
return fmt.Errorf("Error setting %s environment variable to destination value: %s", tokenEnv, err)
}
SetToken(tfTokenDestination)

// Run tf init with new version
tfInit = fmt.Sprintf(`-backend-config=name=%s/%s`, oc.NewOrg, oc.NewName)
Expand Down Expand Up @@ -738,10 +729,6 @@ func RunTFInit(oc OpsConfig, tfToken, tfTokenDestination string) error {
return err
}

if err := os.Setenv(tokenEnv, tfToken); err != nil {
return fmt.Errorf("Error resetting %s environment variable back to source value: %s", tokenEnv, err)
}

return nil
}

Expand Down Expand Up @@ -809,15 +796,18 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) {
}

if cfg.DifferentDestinationAccount {
config.token = cfg.AtlasTokenDestination
// save primary token and set destination token to create the workspace and variables
primaryToken := config.token
SetToken(cfg.AtlasTokenDestination)
_, err := CreateWorkspace(oc, cfg.NewVCSTokenID)
if err != nil {
return nil, err
}
CreateAllVariables(oc.NewOrg, oc.NewName, tfVars)
SetToken(primaryToken)

if cfg.CopyState {
if err := RunTFInit(oc, cfg.AtlasToken, cfg.AtlasTokenDestination); err != nil {
if err := RunTFInit(oc, cfg.AtlasTokenDestination); err != nil {
return sensitiveVars, err
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ func EnableReadOnlyMode() {
func SetToken(t string) {
config.token = t
}

func GetToken() string {
return config.token
}

0 comments on commit e1fae67

Please sign in to comment.