Skip to content

Instantly share code, notes, and snippets.

@fvoges
Created December 8, 2022 14:14
Show Gist options
  • Select an option

  • Save fvoges/efe9119f8244e7a6645481d696864d4f to your computer and use it in GitHub Desktop.

Select an option

Save fvoges/efe9119f8244e7a6645481d696864d4f to your computer and use it in GitHub Desktop.
Example Groovy pipeline TFE and Vault integration for Jenkins
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
def getWorkspaceId() {
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
url: 'https://app.terraform.io/api/v2/organizations/' + env.TFE_ORGANIZATION + '/workspaces/' + env.TFE_WORKSPACE_NAME
)
def data = new JsonSlurper().parseText(response.content)
if (!data.data.id) {
println 'cannot find workspace'
error('cannot find workspace')
}else {
println 'found workspace'
println ('Workspace Id: ' + data.data.id)
return data.data.id
}
}
def getWorkspaceVars(workspace_id, var_keys) {
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
url: 'https://app.terraform.io/api/v2/workspaces/' + workspace_id + '/vars'
)
Map data = new JsonSlurper().parseText(response.content)
Map toreturn = [:]
for (item in data.data) {
if (var_keys.contains(item.attributes.key)) {
toreturn[item.attributes.key] = item.id
}
}
return toreturn
}
def updateWorkspaceVar(workspace_id, var_id, var_key, var_value) {
def payload = """
{
"data": {
"id":"${var_id}",
"attributes": {
"key":"${var_key}",
"value":"${var_value}",
"category":"env",
"hcl": false,
"sensitive": true
},
"type":"vars"
}
}
"""
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'PATCH',
requestBody: "${payload}",
url: 'https://app.terraform.io/api/v2/workspaces/' + workspace_id + '/vars/' + var_id
)
}
def uploadConfigurations(workspace_id) {
def payload = '''
{
"data": {
"type": "configuration-versions",
"attributes": {
"auto-queue-runs": false
}
}
}
'''
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'POST',
requestBody: "${payload}",
url: 'https://app.terraform.io/api/v2/workspaces/' + workspace_id + '/configuration-versions'
)
def data = new JsonSlurper().parseText(response.content)
upload_url = data.data.attributes['upload-url']
println upload_url
return upload_url
}
def startRun(workspace_id) {
def payload = """
{
"data": {
"attributes": {
"is-destroy":false,
"message": "Triggered run from Jenkins (build #${env.BUILD_NUMBER})"
},
"type":"runs",
"relationships": {
"workspace": {
"data": {
"type": "workspaces",
"id": "${workspace_id}"
}
}
}
}
}
"""
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'POST',
requestBody: "${payload}",
url: 'https://app.terraform.io/api/v2/runs'
)
def data = new JsonSlurper().parseText(response.content)
println ('Run Id: ' + data.data.id)
return data.data.id
}
def getAWSCreds(role) {
def payload = '''
{
"ttl":"3600s"
}
'''
def response = httpRequest(
customHeaders: [
[ name: 'X-VAULT-TOKEN', value: env.VAULT_TOKEN ],
[ name: 'Content-Type', value: 'text/plain' ]
],
httpMode: 'PUT',
requestBody: "${payload}",
url: env.VAULT_ADDR + '/v1/aws/sts/' + role
)
def data = new JsonSlurper().parseText(response.content)
Map toreturn = [:]
for (item in data.data.keySet()) {
toreturn[item] = data.data.get(item)
}
return toreturn
}
def waitForPlan(runid) {
def status = ''
while (status != 'errored') {
status = getPlanStatus(runid)
println('Status: ' + status)
// // If a policy requires an override, prompt in the pipeline
// if (status == 'finished') {
// def getPlanResults = getPlanResults(runid)
// return getPlanResults
// }
switch (status) {
case 'finished':
def getPlanResults = getPlanResults(runid)
return getPlanResults
case 'errored':
println 'Plan failed'
return 0
}
sleep(2)
}
}
def getPlanStatus(runid) {
def result = ''
def response = httpRequest(
customHeaders: [[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ]],
url: "https://app.terraform.io/api/v2/runs/${runid}/plan"
)
def data = new JsonSlurper().parseText(response.content)
result = data.data.attributes.status
return result
}
def getPlanResults(runid) {
def result = ''
def response = httpRequest(
customHeaders: [[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ]],
url: "https://app.terraform.io/api/v2/runs/${runid}/plan"
)
def data = new JsonSlurper().parseText(response.content)
def planResults = data.data.attributes.'log-read-url'
return planResults
}
def waitForRun(runid) {
def count = 0
while (true) {
def status = getRunStatus(runid)
println('Status: ' + status)
// If a policy requires an override, prompt in the pipeline
if (status.startsWith('approve_policy')) {
def override
try {
override = input (message: 'Override policy?',
ok: 'Continue',
parameters: [ booleanParam(
defaultValue: false,
description: 'A policy restriction is enforced on this workspace. Check the box to approve overriding the policy.',
name: 'Override')
])
} catch (err) {
override = false
}
// If we're overriding, tell terraform. Otherwise, discard the run
if (override == true) {
println('Overriding!')
def item = status.split(':')[1]
println ('item is ' + item)
def overridden = overridePolicy(item)
if (!overridden) {
println('Could not override the policy')
discardRun(runid)
error('Could not override the Sentinel policy')
break
}
} else {
println('Rejecting!')
discardRun(runid)
error('The pipeline failed due to a Sentinel policy restriction.')
break
}
}
if (status == 'planned_and_finished') {
println ('no changes to be made')
error('no changes to be made')
}
if (status == 'cost_estimated') {
def apply
try {
apply = input (message: 'Confirm Apply', ok: 'Continue',
parameters: [booleanParam(defaultValue: false,
description: 'Would you like to continue to apply this run?', name: 'Apply')])
} catch (err) {
apply = false
}
if (apply == true) {
println('Applying plan')
applyRun(runid)
break
}
else {
println('Rejecting!')
discardRun(runid)
error('The pipeline failed due to a manual rejection of the plan.')
break
}
}
// If we're ready to apply, prompt in the pipeline to do so
if (status == 'apply_plan') {
def apply
try {
apply = input (message: 'Confirm Apply', ok: 'Continue',
parameters: [booleanParam(defaultValue: false,
description: 'Would you like to continue to apply this run?', name: 'Apply')])
} catch (err) {
apply = false
}
// If we're going to apply, tell Terraform. Otherwise, discard the run
if (apply == true) {
println('Applying plan')
applyRun(runid)
break
}
else {
println('Rejecting!')
discardRun(runid)
error('The pipeline failed due to a manual rejection of the plan.')
break
}
}
if (count > 60) break
count++
sleep(2)
}
}
def discardRun(runid) {
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'POST',
responseBody: '{ comment: "Run has been discarded" }',
url: "https://app.terraform.io/api/v2/runs/${runid}/actions/discard"
)
}
def getRunStatus(runid) {
def result = ''
def response = httpRequest(
customHeaders: [[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ]],
url: "https://app.terraform.io/api/v2/runs/${runid}"
)
def data = new JsonSlurper().parseText(response.content)
switch (data.data.attributes.status) {
case 'pending':
case 'plan_queued':
result = 'pending'
break
case 'planning':
result = 'planning'
break
case 'planned':
result = 'planned'
break
case 'cost_estimating':
result = 'costing'
case 'cost_estimated':
result = 'cost_estimated'
break
case 'policy_checking':
result = 'policy'
break
case 'policy_override':
println(response.content)
result = 'approve_policy:' + data.data.relationships['policy-checks'].data[0].id
break
case 'policy_checked':
result = 'apply_plan'
break
case 'planned_and_finished':
result = 'planned_and_finished'
break
default:
result = 'running'
break
}
return result
}
def waitForApply(runid) {
def count = 0
while (true) {
def status = getApplyStatus(runid)
println('Status: ' + status)
if (status == 'discarded') {
println('This run has been discarded')
error('The Terraform run has been discarded, and the pipeline cannot continue.')
break
}
if (status == 'canceled') {
println('This run has been canceled outside the pipeline')
error('The Terraform run has been canceled outside the pipeline, and the pipeline cannot continue.')
break
}
if (status == 'errored') {
println('This run has encountered an error while applying')
error('The Terraform run has encountered an error while applying, and the pipeline cannot continue.')
break
}
if (status == 'applied') {
println('This run has finished applying')
break
}
if (count > 60) break
count++
sleep(2)
}
}
def overridePolicy(policyid) {
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'POST',
url: "https://app.terraform.io/api/v2/policy-checks/${policyid}/actions/override"
)
def data = new JsonSlurper().parseText(response.content)
if (data.data.attributes.status != 'overridden') {
return false
}
else {
return true
}
}
def getApplyStatus(runid) {
def result = ''
def response = httpRequest(
customHeaders: [[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ]],
url: "https://app.terraform.io/api/v2/runs/${runid}"
)
def data = new JsonSlurper().parseText(response.content)
result = data.data.attributes.status
return result
}
def mvn(args) {
withMaven() {
sh "mvn $args"
}
}
def applyRun(runid) {
def response = httpRequest(
customHeaders: [
[ name: 'Authorization', value: 'Bearer ' + env.TFE_TOKEN ],
[ name: 'Content-Type', value: 'application/vnd.api+json' ]
],
httpMode: 'POST',
responseBody: '{ comment: "Apply confirmed" }',
url: "https://app.terraform.io/api/v2/runs/${runid}/actions/apply"
)
}
pipeline {
agent {
node {
label 'master'
}
}
environment {
VAULT_ADDR = 'http://host.docker.internal:8201'
TFE_URL = 'https://app.terraform.io/api/v2/'
TFE_WORKSPACE_NAME = 'jenkinsdemo-dev'
TFE_ORGANIZATION = "${TFE_ORGANIZATION}"
ROLE_NAME = "${ROLE_NAME}"
ROLE_ID = credentials('APPROLE')
SECRET_ID = credentials('SECRET_ID')
}
stages {
stage('setup') {
steps {
script {
env.VAULT_TOKEN = sh (
script: 'vault write -format=json auth/approle/login \
role_id=$ROLE_ID secret_id=$SECRET_ID | jq -r .auth.client_token',
returnStdout: true
).trim()
}
script {
TFE_TOKEN = sh (
script: 'vault read -format=json terraform/creds/jenkins-user | jq -r \'.data.token\'',
returnStdout: true
).trim()
env.TFE_TOKEN = TFE_TOKEN
sh """
echo $BUILD_NUMBER
"""
}
}
}
// stage('getCode') {
// steps {
// dir('tf') {
// git branch: 'main', credentialsId: 'github_pipeline', url: 'https://github.com/hashicorp/notebooks'
// }
// }
// }
stage('getWorkspaceDetails') {
steps {
script {
WORKSPACE_ID = getWorkspaceId()
}
script {
TFE_VARS = getWorkspaceVars(WORKSPACE_ID, ['AWS_SECRET_ACCESS_KEY', 'AWS_ACCESS_KEY_ID', 'AWS_SESSION_TOKEN'])
// println TFE_VARS
}
}
}
stage('uploadConfiguration') {
steps {
script {
upload_url = uploadConfigurations(WORKSPACE_ID)
sh """#!/bin/bash
tar -czf tf.tar.gz -C /terraform/hashicat-dev/ --exclude .git --exclude .terraform .
curl -s --header "Content-Type: application/octet-stream" --request PUT --data-binary @tf.tar.gz $upload_url
"""
}
}
}
stage('RotateWorkspaceTFECreds') {
steps {
script {
creds = getAWSCreds('jenkins-hashicat-pipeline-role')
for (item in TFE_VARS.keySet()) {
if (item == 'AWS_SESSION_TOKEN') {
updateWorkspaceVar(WORKSPACE_ID, TFE_VARS.get(item), item, creds.security_token)
}
if (item == 'AWS_SECRET_ACCESS_KEY') {
updateWorkspaceVar(WORKSPACE_ID, TFE_VARS.get(item), item, creds.secret_key)
}
if (item == 'AWS_ACCESS_KEY_ID') {
updateWorkspaceVar(WORKSPACE_ID, TFE_VARS.get(item), item, creds.access_key)
}
}
}
}
}
stage('WorkspaceRun') {
steps {
// sh 'tfx run create -w ${TFE_WORKSPACE_NAME}'
script {
PLAN_ID = startRun(WORKSPACE_ID)
}
}
}
stage('WorkspacePlanCheck') {
steps {
script {
plan_url = waitForPlan(PLAN_ID)
println(plan_url)
sh """ #!/bin/bash
echo
echo 'Downloading Plan File for build ${BUILD_NUMBER}'
wget -O ${BUILD_NUMBER}.txt $plan_url
"""
}
}
}
stage('WorkspacePolicyCheck') {
steps {
script {
waitForRun(PLAN_ID)
}
}
}
stage('WorkspaceApply') {
steps {
script {
waitForApply(PLAN_ID)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment