Skip to content

Commit

Permalink
Merge pull request #77 from silinternational/release/v4.0.0
Browse files Browse the repository at this point in the history
Release 4.0.0
  • Loading branch information
briskt authored Oct 2, 2024
2 parents d485ca1 + 1c80ff8 commit 03cbff7
Show file tree
Hide file tree
Showing 12 changed files with 250 additions and 181 deletions.
52 changes: 31 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
10 changes: 5 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
Expand Down
23 changes: 14 additions & 9 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 Expand Up @@ -176,7 +182,6 @@ Usage:
Flags:
-a, --attribute string required - Workspace attribute to update, use Terraform Cloud API workspace attribute names
-d, --dry-run-mode dry run mode only. (e.g. "-d")
-h, --help help for update
-v, --value string required - Value
-w, --workspace string required - Partial workspace name to search across all workspaces
Expand Down
77 changes: 71 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,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 != "" {
Expand Down
10 changes: 0 additions & 10 deletions cmd/variablesUpdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ func init() {
false,
`optional (e.g. "-v=true") whether to do the search based on the value of the variables. (Must be false if add-key-if-not-found is true`,
)
updateCmd.Flags().BoolVarP(
&readOnlyMode,
"dry-run-mode",
"d",
false,
`optional (e.g. "-d=true") dry run mode only.`,
)
if err := updateCmd.Flags().MarkDeprecated("dry-run-mode", "use -r for read-only-mode"); err != nil {
errLog.Fatalln(err)
}
updateCmd.Flags().BoolVarP(
&sensitiveVariable,
"sensitive-variable",
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
7 changes: 1 addition & 6 deletions cmd/workspacesUpdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,7 @@ func init() {
requiredPrefix+"Value")
workspaceUpdateCmd.Flags().StringVarP(&workspaceFilter, flagWorkspaceFilter, "w", "",
requiredPrefix+"Partial workspace name to search across all workspaces")
workspaceUpdateCmd.Flags().BoolVarP(&readOnlyMode, "dry-run-mode", "d", false,
`dry run mode only. (e.g. "-d")`,
)
if err := updateCmd.Flags().MarkDeprecated("dry-run-mode", "use -r for read-only-mode"); err != nil {
errLog.Fatalln(err)
}

requiredFlags := []string{flagAttribute, flagValue, flagWorkspaceFilter}
for _, flag := range requiredFlags {
if err := workspaceUpdateCmd.MarkFlagRequired(flag); err != nil {
Expand Down
20 changes: 8 additions & 12 deletions lib/apiClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package lib

import (
"fmt"
"io/ioutil"
"io"
"net/http"
"os"
"strings"
)

// callAPI creates a http.Request object, attaches headers to it and makes the
// requested api call.
func callAPI(method, url, postData string, headers map[string]string) *http.Response {
func callAPI(method, url, postData string, headers map[string]string) (*http.Response, error) {
var err error
var req *http.Request

Expand All @@ -21,8 +20,7 @@ func callAPI(method, url, postData string, headers map[string]string) *http.Resp
}

if err != nil {
fmt.Println(err.Error())
os.Exit(1)
return nil, err
}

req.Header.Set("Authorization", "Bearer "+config.token)
Expand All @@ -36,15 +34,13 @@ func callAPI(method, url, postData string, headers map[string]string) *http.Resp

resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
return nil, err
} else if resp.StatusCode >= 300 {
bodyBytes, _ := ioutil.ReadAll(resp.Body)
fmt.Println(fmt.Sprintf(
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf(
"API returned an error.\n\tMethod: %s\n\tURL: %s\n\tCode: %v\n\tStatus: %s\n\tRequest Body: %s\n\tResponse Body: %s",
method, url, resp.StatusCode, resp.Status, postData, bodyBytes))
os.Exit(1)
method, url, resp.StatusCode, resp.Status, postData, bodyBytes)
}

return resp
return resp, nil
}
Loading

0 comments on commit 03cbff7

Please sign in to comment.