Created
December 8, 2022 14:14
-
-
Save fvoges/efe9119f8244e7a6645481d696864d4f to your computer and use it in GitHub Desktop.
Example Groovy pipeline TFE and Vault integration for Jenkins
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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