Introduction
This article describes a process for mass replacing VCS OAuth tokens in Terraform Cloud or Enterprise using the script vcs-update-script.sh
, attached below.
Use Case
The use case for this procedure is when a user needs to update the OAuth token of multiple workspaces in Terraform Cloud or Enterprise, for example:
- The current OAuth token has been compromised or is no longer valid
- The current OAuth token has been revoked by the VCS provider
- The OAuth token expires due to a bug or outage
Procedure
- Create an API token from a user with enough permissions to update the workspace VCS settings.
- In the shell, set these variables
export TOKEN=<YOUR_TFE_API_TOKEN> export ORG=<YOUR_ORGANIZATION_NAME> export TFE_HOSTNAME=<YOUR_TFE_HOSTNAME>
Note: If using Terraform Cloud,TFE_HOSTNAME
isapp.terraform.io
- Run this
curl
command to get the organization OAuth clients:
curl \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/vnd.api+json" \ --request GET \ https://$TFE_HOSTNAME/api/v2/organizations/$ORG/oauth-clients
From the output of the curl command (piping it throughjq -r
may make it easier to read), locate the OAuth token of the VCS connection that you wish use going forward. This should have a relationships object with oauth-tokens. Within this, there should be anoauth-tokens
type with an id prefixed byot-
. Grab this and setOAUTH_TOKEN
to it with exportOAUTH_TOKEN=<OAUTH_TOKEN_VCS>
. As an example of the output, trimmed down for clarity. In this example, we wantOAUTH_TOKEN=ot-R9dcvasfasnny5pRFH
:
{ "data":[ { "id":"oc-zpYpgyFj9FWe4Vih", "type":"oauth-clients", "attributes":{ "name":"Test ADO", "created-at":"2022-06-08T19:42:29.291Z", "callback-url":"https://app.terraform.io/auth/dbb55ab5-8f56-4bcf-9b4e-asdgjhasdhfkj/callback", "service-provider":"ado_services", "service-provider-display-name":"Azure DevOps Services", "http-url":"https://dev.azure.com", "api-url":"https://dev.azure.com" }, "relationships":{ "organization":{ "data":{ "id":"orgnamehere", "type":"organizations" }, "links":{ "related":"/api/v2/organizations/orgnamehere" } }, "oauth-tokens":{ "data":[ { "id":"ot-R9dcvasfasnny5pRFH", "type":"oauth-tokens" } ] } } } ] }
- Set the
OAUTH_TOKEN
environment variable to the OAuth token you found in step 3.export OAUTH_TOKEN=<OAUTH-TOKEN-ID>
- Run this curl command to get a list of workspaces within an organization and place them in the file
output.json
:
curl \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/vnd.api+json" \ "https://$TFE_HOSTNAME/api/v2/organizations/$ORG/workspaces?page%5Bnumber%5D=1&page%5Bsize%5D=100" |\ jq -r '[.data[].attributes | {workspace: .name, repo: ."vcs-repo"."display-identifier"}]' > output.json
Please review the workspace list inoutput.json
as it might need to be edited to remove workspaces from different VCS providers. The API call above might also be able to be adjusted to filter these out. - Run the
vcs-update-script.sh
script, passing the fileoutput.json
as an argument.
./vcs-update-script.sh output.json
- The output will look something like this:
Setting VCS connection for workspace1 to VCSOrg/workspace1 Success! Setting VCS connection for workspace2 to VCSOrg/workspace2 Success!
If there is an error in the API call, it will log. In this example, the repo is long gone from a previous reproduction:
Setting VCS connection for test-vcs-tfc to testorg/test-vcs-tfc/test-vcs-tfc Something went wrong: {"errors":[{"status":"422","title":"invalid attribute","detail":"Repository doesn't exist or isn't accessible","source":{"pointer":"/data/attributes/repository"}}]}
For CLI-driven workspaces, the API call will set a null value for the VCS OAuth token (which would already be null), so these process just fine.
Additional Information
The script vcs-update-script.sh
uses the Terraform Enterprise API to update the VCS repos of the workspaces specified in the input file. The script makes API calls to the Terraform Enterprise instance specified by the TFE_HOSTNAME
environment variable, using the user token specified by the TOKEN
environment variable and the organization specified by the ORG
environment variable. The OAuth token used in the update is specified by the OAUTH_TOKEN
environment variable. The script will output per-workspace "Success!" if the update was successful, or an error message if something went wrong.
Related API Docs
https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#list-workspaces
https://developer.hashicorp.com/terraform/cloud-docs/api-docs/oauth-clients#list-oauth-clients
https://developer.hashicorp.com/terraform/cloud-docs/api-docs/workspaces#update-a-workspace
The Script
#!/bin/bash # Pass the input file to the script or exit INPUT_FILE=$1 if [[ -z $INPUT_FILE ]]; then echo "Usage: $0 file-to-read" exit 1 fi # Make sure all of the required variables are set. # This could be more graceful, but should do the trick if [ -z $TOKEN ] || [ -z $TFE_HOSTNAME ] || [ -z $ORG ] || [ -z $OAUTH_TOKEN ]; then printf "Please make sure the following environment variables are set:\n\n" printf "%-13s %s\n" \ "TOKEN" "A user token with the appropriate permissions to alter a workspace's VCS settings" \ "TFE_HOSTNAME" "The hostname of your Terraform Enterprise instance" \ "ORG" "The organization that the workspaces exists in" \ "OAUTH_TOKEN" "The OAuth token of the VCS connection. Can be found in the organization's VCS settings" exit 1 fi # Break the input file into individual flat objects WORKSPACES=$(jq -c '.[]' $INPUT_FILE) # Iterate over the lines, making the appropriate API call for x in $WORKSPACES; do NAME=$(echo $x | jq -r '.workspace') REPO=$(echo $x | jq -r '.repo') printf "Setting VCS connection for %s to %s \n" $NAME $REPO # Capture and parse the response, looking for the VCS repo identifier. # This allows us to compare the expected result to the *actual* result. RESPONSE=$(curl -s \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/vnd.api+json" \ --request PATCH \ --data "{\"data\":{\"attributes\":{\"vcs-repo\":{\"identifier\":\"$REPO\",\"oauth-token-id\":\"$OAUTH_TOKEN\"}},\"type\":\"workspaces\"}}" \ "https://$TFE_HOSTNAME/api/v2/organizations/$ORG/workspaces/$NAME") PARSED_RESPONSE=$(echo $RESPONSE | jq -r '.data.attributes."vcs-repo".identifier') # Report back whether expectation == reality if [[ $PARSED_RESPONSE == $REPO ]]; then printf "Success!\n\n" else printf "Something went wrong: $RESPONSE\n\n" fi done