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_HOSTNAMEisapp.terraform.io - Run this
curlcommand 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-clientsFrom the output of the curl command (piping it throughjq -rmay 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-tokenstype with an id prefixed byot-. Grab this and setOAUTH_TOKENto 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_TOKENenvironment 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.jsonPlease review the workspace list inoutput.jsonas 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.shscript, passing the fileoutput.jsonas 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