diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1d7e43..6c898fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,24 +13,34 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Fetch all tags - run: git fetch --force --tags - - - name: Set up Go - uses: actions/setup-go@v3 - with: - go-version: 1.19 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Fetch all tags + run: git fetch --force --tags + - + name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + - + name: Run GoReleaser in snapshot mode + uses: goreleaser/goreleaser-action@v5 + if: github.event.pull_request + with: + version: '~> v1' + args: release --snapshot --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - + name: Run GoReleaser on a release tag + uses: goreleaser/goreleaser-action@v5 + if: startsWith(github.ref, 'refs/tags/') + with: + version: '~> v1' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5ae19f..d5deba7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,20 +1,20 @@ name: Test on: - pull_request: push: + branches: [ "**" ] jobs: - build: + test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version-file: 'go.mod' - name: Build run: go build -v ./... diff --git a/README.md b/README.md index ac24a75..d439d3a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/cmd/root.go b/cmd/root.go index cf75c68..7b2287d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,9 +15,13 @@ package cmd import ( + "encoding/json" + "errors" "fmt" "log" "os" + "path/filepath" + "runtime" "strings" "github.com/spf13/cobra" @@ -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" { @@ -76,6 +75,72 @@ 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) { + userConfigDir := os.UserHomeDir + if runtime.GOOS == "windows" { + userConfigDir = os.UserConfigDir + } + + var err error + configDir, err := userConfigDir() + if err != nil { + return nil, fmt.Errorf("unable to get the home directory: %v", err) + } + + credentialsPath := filepath.Join(configDir, ".terraform.d", "credentials.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 != "" { diff --git a/cmd/workspacesClone.go b/cmd/workspacesClone.go index 5657145..2631d70 100644 --- a/cmd/workspacesClone.go +++ b/cmd/workspacesClone.go @@ -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, "+ diff --git a/lib/client.go b/lib/client.go index 486dcd9..9d8284c 100644 --- a/lib/client.go +++ b/lib/client.go @@ -48,7 +48,6 @@ type CloneConfig struct { SourceWorkspace string NewWorkspace string NewVCSTokenID string - AtlasToken string AtlasTokenDestination string CopyState bool CopyVariables bool @@ -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 @@ -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) @@ -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) @@ -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 } @@ -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 } } diff --git a/lib/config.go b/lib/config.go index aa68cc6..ebe7d06 100644 --- a/lib/config.go +++ b/lib/config.go @@ -19,3 +19,7 @@ func EnableReadOnlyMode() { func SetToken(t string) { config.token = t } + +func GetToken() string { + return config.token +}