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 9283493..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. @@ -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 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/variablesUpdate.go b/cmd/variablesUpdate.go index 78ac511..4aead20 100644 --- a/cmd/variablesUpdate.go +++ b/cmd/variablesUpdate.go @@ -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", 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/cmd/workspacesUpdate.go b/cmd/workspacesUpdate.go index 838c9b3..5565fa6 100644 --- a/cmd/workspacesUpdate.go +++ b/cmd/workspacesUpdate.go @@ -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 { diff --git a/lib/apiClient.go b/lib/apiClient.go index 47dadd8..2d8d955 100644 --- a/lib/apiClient.go +++ b/lib/apiClient.go @@ -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 @@ -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) @@ -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 } diff --git a/lib/client.go b/lib/client.go index 26e329f..9d8284c 100644 --- a/lib/client.go +++ b/lib/client.go @@ -20,7 +20,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "log" "net/http" "os" "os/exec" @@ -48,7 +48,6 @@ type CloneConfig struct { SourceWorkspace string NewWorkspace string NewVCSTokenID string - AtlasToken string AtlasTokenDestination string CopyState bool CopyVariables bool @@ -229,14 +228,6 @@ type AllTeamWorkspaceData struct { Data []TeamWorkspaceData `json:"data"` } -// TFVar matches the attributes of a terraform environment/workspace's variable -type TFVar struct { - Key string `json:"key"` - Value string `json:"value"` - Hcl bool `json:"hcl"` - Sensitive bool `json:"sensitive"` -} - type WorkspaceUpdateParams struct { Organization string WorkspaceFilter string @@ -244,9 +235,9 @@ type WorkspaceUpdateParams struct { Value string } -// ConvertHCLVariable changes a TFVar struct in place by escaping +// ConvertHCLVariable changes a Var struct in place by escaping // the double quotes and line endings in the Value attribute -func ConvertHCLVariable(tfVar *TFVar) { +func ConvertHCLVariable(tfVar *Var) { if !tfVar.Hcl { return } @@ -257,7 +248,7 @@ func ConvertHCLVariable(tfVar *TFVar) { // GetCreateVariablePayload returns the json needed to make a Post to the // Terraform vars api -func GetCreateVariablePayload(organization, workspaceName string, tfVar TFVar) string { +func GetCreateVariablePayload(organization, workspaceName string, tfVar Var) string { return fmt.Sprintf(` { "data": { @@ -284,7 +275,7 @@ func GetCreateVariablePayload(organization, workspaceName string, tfVar TFVar) s // GetUpdateVariablePayload returns the json needed to make a Post to the // Terraform vars api -func GetUpdateVariablePayload(organization, workspaceName, variableID string, tfVar TFVar) string { +func GetUpdateVariablePayload(organization, workspaceName, variableID string, tfVar Var) string { return fmt.Sprintf(` { "data": { @@ -318,7 +309,10 @@ func OrganizationExists(organization string) (bool, error) { } u := NewTfcUrl("/organizations/" + organization) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return false, err + } defer resp.Body.Close() @@ -350,14 +344,13 @@ func GetAllWorkspaces(organization string) ([]Workspace, error) { } func getWorkspacePage(url string) (WorkspaceList, error) { - resp := callAPI(http.MethodGet, url, "", nil) - + resp, err := callAPI(http.MethodGet, url, "", nil) + if err != nil { + return WorkspaceList{}, err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var nextWsData WorkspaceList - if err := json.NewDecoder(resp.Body).Decode(&nextWsData); err != nil { return WorkspaceList{}, fmt.Errorf("json decode error: %s", err) } @@ -377,14 +370,13 @@ func GetWorkspaceData(organization, workspaceName string) (WorkspaceJSON, error) workspaceName, )) - resp := callAPI(http.MethodGet, u.String(), "", nil) - + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return WorkspaceJSON{}, err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var wsData WorkspaceJSON - if err := json.NewDecoder(resp.Body).Decode(&wsData); err != nil { return WorkspaceJSON{}, fmt.Errorf("Error getting workspace data for %s:%s\n%s", organization, workspaceName, err.Error()) } @@ -425,14 +417,13 @@ func GetVarsFromWorkspace(organization, workspaceName string) ([]Var, error) { u.SetParam(paramFilterOrganizationName, organization) u.SetParam(paramFilterWorkspaceName, workspaceName) - resp := callAPI(http.MethodGet, u.String(), "", nil) - + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return nil, err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var varsResp VarsResponse - if err := json.NewDecoder(resp.Body).Decode(&varsResp); err != nil { return nil, fmt.Errorf("Error getting variables for %s:%s ...\n%s", organization, workspaceName, err.Error()) } @@ -450,7 +441,10 @@ func GetVarsFromWorkspace(organization, workspaceName string) ([]Var, error) { func DeleteVariable(variableID string) { u := NewTfcUrl("/vars/" + variableID) - resp := callAPI(http.MethodDelete, u.String(), "", nil) + resp, err := callAPI(http.MethodDelete, u.String(), "", nil) + if err != nil { + log.Fatalln(err) + } _ = resp.Body.Close() } @@ -498,15 +492,16 @@ func SearchVariables(organization, wsName, keyContains, valueContains string) ([ // GetTeamAccessFrom returns the team access data from an existing workspace func GetTeamAccessFrom(workspaceID string) (AllTeamWorkspaceData, error) { - u := NewTfcUrl(fmt.Sprintf("/team-workspaces")) + u := NewTfcUrl("/team-workspaces") u.SetParam(paramFilterWorkspaceID, workspaceID) - resp := callAPI(http.MethodGet, u.String(), "", nil) - + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return AllTeamWorkspaceData{}, err + } defer resp.Body.Close() var allTeamData AllTeamWorkspaceData - if err := json.NewDecoder(resp.Body).Decode(&allTeamData); err != nil { return AllTeamWorkspaceData{}, fmt.Errorf("Error getting team workspace data for %s\n%s", workspaceID, err.Error()) } @@ -552,32 +547,33 @@ func AssignTeamAccess(workspaceID string, allTeamData AllTeamWorkspaceData) { teamData.Relationships.Team.Data.ID, ) - resp := callAPI(http.MethodPost, url, postData, nil) + resp, err := callAPI(http.MethodPost, url, postData, nil) + if err != nil { + log.Fatalln(err) + } defer resp.Body.Close() } - return } // CreateVariable makes a Terraform vars API POST to create a variable // for a given organization and workspace -func CreateVariable(organization, workspaceName string, tfVar TFVar) { +func CreateVariable(organization, workspaceName string, tfVar Var) { url := baseURL + "/vars" ConvertHCLVariable(&tfVar) postData := GetCreateVariablePayload(organization, workspaceName, tfVar) - resp := callAPI(http.MethodPost, url, postData, nil) - + resp, err := callAPI(http.MethodPost, url, postData, nil) + if err != nil { + log.Fatalln(err) + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) - return } // CreateAllVariables makes several Terraform vars API POSTs to create // variables for a given organization and workspace -func CreateAllVariables(organization, workspaceName string, tfVars []TFVar) { +func CreateAllVariables(organization, workspaceName string, tfVars []Var) { for _, nextVar := range tfVars { CreateVariable(organization, workspaceName, nextVar) } @@ -606,19 +602,19 @@ func GetCreateWorkspacePayload(oc OpsConfig, vcsTokenID string) string { // UpdateVariable makes a Terraform vars API call to update a variable // for a given organization and workspace -func UpdateVariable(organization, workspaceName, variableID string, tfVar TFVar) { +func UpdateVariable(organization, workspaceName, variableID string, tfVar Var) { url := fmt.Sprintf(baseURL+"/vars/%s", variableID) ConvertHCLVariable(&tfVar) patchData := GetUpdateVariablePayload(organization, workspaceName, variableID, tfVar) - resp := callAPI(http.MethodPatch, url, patchData, nil) + resp, err := callAPI(http.MethodPatch, url, patchData, nil) + if err != nil { + log.Fatalln(err) + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) - return } // CreateWorkspace makes a Terraform workspaces API call to create a @@ -631,14 +627,13 @@ func CreateWorkspace(oc OpsConfig, vcsTokenID string) (string, error) { postData := GetCreateWorkspacePayload(oc, vcsTokenID) - resp := callAPI(http.MethodPost, url, postData, nil) - + resp, err := callAPI(http.MethodPost, url, postData, nil) + if err != nil { + return "", err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var wsData WorkspaceJSON - if err := json.NewDecoder(resp.Body).Decode(&wsData); err != nil { return "", fmt.Errorf("error getting created workspace data: %s\n", err) } @@ -655,14 +650,13 @@ func CreateWorkspace2(oc OpsConfig, vcsTokenID string) (Workspace, error) { postData := GetCreateWorkspacePayload(oc, vcsTokenID) - resp := callAPI(http.MethodPost, url, postData, nil) - + resp, err := callAPI(http.MethodPost, url, postData, nil) + if err != nil { + return Workspace{}, err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var wsData WorkspaceJSON - if err := json.NewDecoder(resp.Body).Decode(&wsData); err != nil { return Workspace{}, fmt.Errorf("error getting created workspace data: %s\n", err) } @@ -676,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 @@ -695,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) @@ -711,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) @@ -743,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 } @@ -786,18 +768,18 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) { sensitiveValue := "TF_ENTERPRISE_SENSITIVE_VAR" defaultValue := "REPLACE_THIS_VALUE" - tfVars := []TFVar{} - var tfVar TFVar + tfVars := []Var{} + var tfVar Var for _, nextVar := range variables { if cfg.CopyVariables { - tfVar = TFVar{ + tfVar = Var{ Key: nextVar.Key, Value: nextVar.Value, Hcl: nextVar.Hcl, } } else { - tfVar = TFVar{ + tfVar = Var{ Key: nextVar.Key, Value: defaultValue, Hcl: nextVar.Hcl, @@ -814,14 +796,18 @@ func CloneWorkspace(cfg CloneConfig) ([]string, error) { } if cfg.DifferentDestinationAccount { - config.token = cfg.AtlasTokenDestination - if _, err := CreateWorkspace(oc, cfg.NewVCSTokenID); err != nil { + // 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 } } @@ -876,7 +862,7 @@ func AddOrUpdateVariable(cfg UpdateConfig) (string, error) { continue } // Found a match - tfVar := TFVar{Key: nextVar.Key, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} + tfVar := Var{Key: nextVar.Key, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} if !config.readOnly { UpdateVariable(cfg.Organization, cfg.Workspace, nextVar.ID, tfVar) } @@ -894,7 +880,7 @@ func AddOrUpdateVariable(cfg UpdateConfig) (string, error) { return "", errors.New("addKeyIfNotFound was set to true but a variable already exists with key " + nextVar.Key) } - tfVar := TFVar{Key: nextVar.Key, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} + tfVar := Var{Key: nextVar.Key, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} if !config.readOnly { UpdateVariable(cfg.Organization, cfg.Workspace, nextVar.ID, tfVar) @@ -904,7 +890,7 @@ func AddOrUpdateVariable(cfg UpdateConfig) (string, error) { // At this point, we haven't found a match if cfg.AddKeyIfNotFound { - tfVar := TFVar{Key: cfg.SearchString, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} + tfVar := Var{Key: cfg.SearchString, Value: cfg.NewValue, Hcl: false, Sensitive: cfg.SensitiveVariable} if !config.readOnly { CreateVariable(cfg.Organization, cfg.Workspace, tfVar) @@ -943,20 +929,18 @@ type OAuthTokens struct { func getVCSToken(vcsUsername, orgName string) (string, error) { url := fmt.Sprintf(baseURL+"/organizations/%s/oauth-tokens", orgName) - resp := callAPI(http.MethodGet, url, "", nil) - + resp, err := callAPI(http.MethodGet, url, "", nil) + if err != nil { + return "", err + } defer resp.Body.Close() - // bodyBytes, _ := ioutil.ReadAll(resp.Body) - // fmt.Println(string(bodyBytes)) var oauthTokens OAuthTokens - if err := json.NewDecoder(resp.Body).Decode(&oauthTokens); err != nil { return "", err } vcsTokenID := "" - for _, nextToken := range oauthTokens.Data { if nextToken.Attributes.ServiceProviderUser == vcsUsername { vcsTokenID = nextToken.ID @@ -1010,9 +994,13 @@ func UpdateWorkspace(params WorkspaceUpdateParams) error { } for id, name := range foundWs { url := fmt.Sprintf(baseURL+"/workspaces/%s", id) - resp := callAPI(http.MethodPatch, url, postData, nil) - bodyBytes, _ := ioutil.ReadAll(resp.Body) - _ = resp.Body.Close() + resp, err := callAPI(http.MethodPatch, url, postData, nil) + if err != nil { + return err + } + + bodyBytes, _ := io.ReadAll(resp.Body) + defer resp.Body.Close() fmt.Printf("set '%s' to '%s' on workspace %s\n", params.Attribute, params.Value, name) if config.debug { @@ -1059,7 +1047,10 @@ func FindWorkspaces(organization, workspaceFilter string) map[string]string { var attributeData [][]string for page := 1; ; page++ { u.SetParam(paramPageNumber, strconv.Itoa(page)) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + log.Fatalln(err) + } ws := parseWorkspacePage(resp, []string{"id", "name"}) attributeData = append(attributeData, ws...) if len(ws) < pageSize { @@ -1083,7 +1074,11 @@ func GetWorkspaceAttributes(organization string, attributes []string) ([][]strin var attributeData [][]string for page := 1; ; page++ { u.SetParam(paramPageNumber, strconv.Itoa(page)) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + log.Fatalln(err) + } + ws := parseWorkspacePage(resp, attributes) attributeData = append(attributeData, ws...) if len(ws) < pageSize { @@ -1125,7 +1120,10 @@ func parseWorkspacePage(resp *http.Response, attributes []string) [][]string { func GetWorkspaceByName(organizationName, workspaceName string) (Workspace, error) { u := NewTfcUrl(fmt.Sprintf("/organizations/%s/workspaces/%s", organizationName, workspaceName)) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return Workspace{}, err + } var ws WorkspaceJSON if err := json.NewDecoder(resp.Body).Decode(&ws); err != nil { @@ -1196,11 +1194,14 @@ type VariableSetList struct { func GetAllVariableSets(organizationName string) (VariableSetList, error) { u := NewTfcUrl(fmt.Sprintf("/organizations/%s/varsets", organizationName)) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return VariableSetList{}, err + } var variableSetList VariableSetList if err := json.NewDecoder(resp.Body).Decode(&variableSetList); err != nil { - return variableSetList, fmt.Errorf("unexpected content retrieving variable set list: %w", err) + return VariableSetList{}, fmt.Errorf("unexpected content retrieving variable set list: %w", err) } return variableSetList, nil @@ -1230,9 +1231,9 @@ func ApplyVariableSet(varsetID string, workspaceIDs []string) error { if config.readOnly { return nil } - _ = callAPI(http.MethodPost, u.String(), postData, nil) + _, err = callAPI(http.MethodPost, u.String(), postData, nil) // TODO: need to look at response? - return nil + return err } func copyVariableSetList(sourceWorkspaceID, destinationWorkspaceID string) error { @@ -1267,7 +1268,10 @@ func ApplyVariableSetsToWorkspace(sets VariableSetList, workspaceID string) erro func ListWorkspaceVariableSets(workspaceID string) (VariableSetList, error) { u := NewTfcUrl(fmt.Sprintf("/workspaces/%s/varsets", workspaceID)) - resp := callAPI(http.MethodGet, u.String(), "", nil) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) + if err != nil { + return VariableSetList{}, err + } var variableSetList VariableSetList if err := json.NewDecoder(resp.Body).Decode(&variableSetList); err != nil { @@ -1295,7 +1299,6 @@ func AddRemoteStateConsumers(workspaceID string, consumerIDs []string) error { } postData := data.String() - _ = callAPI(http.MethodPost, u.String(), postData, nil) - - return nil + _, err = callAPI(http.MethodPost, u.String(), postData, nil) + return 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 +} diff --git a/lib/run.go b/lib/run.go index bdf62d1..be05da0 100644 --- a/lib/run.go +++ b/lib/run.go @@ -16,8 +16,8 @@ type RunConfig struct { func CreateRun(config RunConfig) error { u := NewTfcUrl("/runs") payload := buildRunPayload(config.Message, config.WorkspaceID) - _ = callAPI(http.MethodPost, u.String(), payload, nil) - return nil + _, err := callAPI(http.MethodPost, u.String(), payload, nil) + return err } func buildRunPayload(message, workspaceID string) string { diff --git a/lib/runtrigger.go b/lib/runtrigger.go index d0361ab..3d88bf0 100644 --- a/lib/runtrigger.go +++ b/lib/runtrigger.go @@ -17,8 +17,8 @@ type RunTriggerConfig struct { func CreateRunTrigger(config RunTriggerConfig) error { u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") payload := buildRunTriggerPayload(config.SourceWorkspaceID) - _ = callAPI(http.MethodPost, u.String(), payload, nil) - return nil + _, err := callAPI(http.MethodPost, u.String(), payload, nil) + return err } func buildRunTriggerPayload(sourceWorkspaceID string) string { @@ -81,12 +81,13 @@ func ListRunTriggers(config ListRunTriggerConfig) ([]RunTrigger, error) { u := NewTfcUrl("/workspaces/" + config.WorkspaceID + "/run-triggers") u.SetParam(paramFilterRunTriggerType, config.Type) - resp := callAPI(http.MethodGet, u.String(), "", nil) - triggers, err := parseRunTriggerListResponse(resp.Body) + resp, err := callAPI(http.MethodGet, u.String(), "", nil) if err != nil { return nil, err } - return triggers, nil + defer resp.Body.Close() + + return parseRunTriggerListResponse(resp.Body) } func parseRunTriggerListResponse(r io.Reader) ([]RunTrigger, error) {