Skip to content

Instantly share code, notes, and snippets.

@straubt1
Last active June 22, 2020 12:52
Show Gist options
  • Save straubt1/b4337278cdd80d1e4becd47f6558f0ad to your computer and use it in GitHub Desktop.
Save straubt1/b4337278cdd80d1e4becd47f6558f0ad to your computer and use it in GitHub Desktop.
TFE Speculative Destroy

TFE Speculative Destroy

Desired Outcome

Assuming a TFE Workspace has been successfully Applied, perform an API driven workflow to queue a speculative destroy plan (mimicking a terraform plan -destroy). In other words, a TFE Run that is a destroy, but unable to actually apply it like a speculative plan.

Steps to Reproduce

  1. Create a new Configuration Version, setting "speculative": true.
  2. Archive the code (or no code??) and upload to the configuration version.
  3. Create a run with the new Configuration Version.
  4. Destroy is queued, but it is NOT speculative.

Ideally I don't have to upload any code to the run since we are destroying, but this is required AFAIK since the CV holds the speculative argument, and you have to upload something to it for run to be processed.

Script

Example run:

python3 tfe-run-destroy.py \
  -terraformWorkingDirectory ../repo-terraform-code \
  -tfeHostName firefly.tfe.rocks \
  -tfeOrganizationName hashicorp \
  -tfeWorkspaceName ado-terraform-dev \
  -tfeToken aaaa.atlasv1.bbbb \
  -tfeSpeculativePlan True

Running the provided script we can see something like the following output:

TFE Workspace Id: ws-RTEywPEaCVJsED9o
Newly Created Configuration Version Id: cv-uAgRCbJD2tTUSZGa
Configuration Version Id sent to trigger the Run: cv-uAgRCbJD2tTUSZGa
Configuration Version Id the Run actually used: cv-uAgRCbJD2tTUSZGa

The CV Id is correct, however the TFE Run that was created was not speculative.

#!/usr/bin/python
import argparse
import json
import os
import tarfile
import requests
# Required, these can be set via arguments or environment variables
parser = argparse.ArgumentParser(
description='Perform a TFE Run Plan.')
parser.add_argument('-tfeToken',
default=os.environ.get('TFETOKEN'),
help='API Token used to authenticate to TFE.')
parser.add_argument('-tfeHostName',
default=os.environ.get('TFEHOSTNAME'),
help="TFE Hostname (i.e. terraform.company.com")
parser.add_argument('-tfeOrganizationName',
default=os.environ.get('TFEORGANIZATIONNAME'),
help="TFE Organization Name (i.e. hashicorp-dev")
parser.add_argument('-tfeWorkspaceName',
default=os.environ.get('TFEWORKSPACENAME'),
help="TFE Workspace Name (i.e. app1-eastus-dev")
parser.add_argument('-terraformWorkingDirectory',
default=os.environ.get('TERRAFORMWORKINGDIRECTORY'),
help='The path of the Terraform code based on current working directory.')
# Optional
parser.add_argument('-tfeArchiveFileName',
default='terraform.tar.gz',
help='The file name to create as the archive file.')
parser.add_argument('-tfeSpeculativePlan',
default='False',
help="When True, trigger a speculative plan that can not be applied.")
# Parse Args
try:
settings = parser.parse_args()
settings_as_dict = vars(settings)
for k in settings_as_dict:
if settings_as_dict[k] is None:
print(f'##[error]Missing argument: {k}')
if None in settings_as_dict.values():
raise Exception('Missing required arguments')
except Exception:
parser.print_help()
raise
# Create archive
currentDirectory = os.getcwd()
archiveFullPath = os.path.join(currentDirectory, settings.tfeArchiveFileName)
# Change working directory to make the tarfile logic easier
os.chdir(settings.terraformWorkingDirectory)
print(f'Generating the tar.gz file')
tar = tarfile.open(archiveFullPath, "w:gz")
for root, dirs, files in os.walk('./', topdown=True):
# skip any potential temp directories
dirs[:] = [d for d in dirs if d not in ['.git', '.terraform']]
for file in files:
# print(f'##[debug]Archiving File: {os.path.join(root, file)}')
tar.add(os.path.join(root, file))
tar.close()
# Revert working directory back
os.chdir(currentDirectory)
# Get Workspace ID
resp = requests.get(
f'https://{settings.tfeHostName}/api/v2/organizations/{settings.tfeOrganizationName}/workspaces/{settings.tfeWorkspaceName}',
headers={'Authorization': f'Bearer {settings.tfeToken}',
'Content-Type': 'application/vnd.api+json'},
)
vars(settings)['tfeWorkspaceId'] = resp.json()['data']['id']
print(f'TFE Workspace Id: {settings.tfeWorkspaceId}')
# Create Config Version
tfConfig = {
"data":
{
"type": "configuration-versions",
"attributes": {
"auto-queue-runs": False,
"speculative": settings.tfeSpeculativePlan
}
}
}
resp = requests.post(
f'https://{settings.tfeHostName}/api/v2/workspaces/{settings.tfeWorkspaceId}/configuration-versions',
headers={'Authorization': f'Bearer {settings.tfeToken}',
'Content-Type': 'application/vnd.api+json'},
data=json.dumps(tfConfig)
)
vars(settings)['tfeConfigurationVersionId'] = resp.json()['data']['id']
vars(settings)['tfeConfigurationVersionUploadUrl'] = resp.json()['data']['attributes']['upload-url']
# Upload tar to configuration version
resp = requests.put(settings.tfeConfigurationVersionUploadUrl,
headers={'Authorization': f'Bearer {settings.tfeToken}',
'Content-Type': 'application/octet-stream'},
data=open(settings.tfeArchiveFileName, 'rb').read()
)
# Create Destroy Plan
tfConfig = {
"data": {
"type": "runs",
"attributes": {
"is-destroy": True,
"message": "Triggered Destroy"
},
"relationships": {
"workspace": {
"data": {
"type": "workspaces",
"id": settings.tfeWorkspaceId
}
},
"configuration-version": {
"data": {
"type": "configuration-versions",
"id": settings.tfeConfigurationVersionId
}
}
}
}
}
resp = requests.post(f'https://{settings.tfeHostName}/api/v2/runs',
headers={'Authorization': f'Bearer {settings.tfeToken}',
'Content-Type': 'application/vnd.api+json'},
data=json.dumps(tfConfig)
)
print(f'Newly Created Configuration Version Id: {settings.tfeConfigurationVersionId}')
print(
f'Configuration Version Id sent to trigger the Run: {tfConfig["data"]["relationships"]["configuration-version"]["data"]["id"]}')
print(
f'Configuration Version Id the Run actually used: {resp.json()["data"]["relationships"]["configuration-version"]["data"]["id"]}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment