Skip to content

Commit

Permalink
Manage Gandi DNS with Terraform - sandbox only
Browse files Browse the repository at this point in the history
Signed-off-by: Benoit Donneaux <[email protected]>
  • Loading branch information
btlogy committed Oct 10, 2024
1 parent c33bd6d commit 5e118cb
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 0 deletions.
195 changes: 195 additions & 0 deletions .github/workflows/_terraform.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Re-usable workflow to continuously integrate and deploy Terraform plan

on:
workflow_call:
inputs:
tf_version:
description: 'Version of Terraform runtime to use'
required: false
type: string
default: '1.7.4'
tf_dir:
description: 'Version of Terraform runtime to use'
required: false
type: string
default: './'
gh_runner_version:
description: 'Version of the GitHub runner to use'
required: false
type: string
default: 'ubuntu-24.04'
auto_comment:
description: 'Enable automatic comment on GitHub pull request'
required: false
type: boolean
default: true
apply_on_branch:
description: 'Automaticaly apply plan when on a specific branch'
required: false
type: string
default: ''
gandi_url:
description: 'Gandi API URL'
required: false
type: string
default: 'https://api.gandi.net'
secrets:
gandi_token:
description: 'API token for Gandi'
required: false
hcloud_token:
description: 'API token for Hetzner Cloud'
required: false

jobs:
terraform:
name: Terraform
runs-on: ${{ inputs.gh_runner_version }}
defaults:
run:
working-directory: ${{ inputs.tf_dir }}
env:
TF_VAR_gandi_token: ${{ secrets.gandi_token }}
TF_VAR_hcloud_token: ${{ secrets.hcloud_token }}
permissions:
pull-requests: write
contents: read

steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4

- name: Setup
id: setup
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.tf_version }}

- name: Format
id: fmt
run: terraform fmt -no-color -check -diff

- name: Init
id: init
run: terraform init -no-color -input=false

- name: Validate
id: validate
run: terraform validate -no-color

- name: Plan
id: plan
run: terraform plan -no-color -input=false -compact-warnings -out terraform_plan.out

- name: Post Setup
id: post_setup
# Only to avoid wrapper to mess up outputs in later steps
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ inputs.tf_version }}
terraform_wrapper: false

- name: Verify
id: verify
run: |
# Process the plan to verify the presence of some data
# which can be used later to make additional checks
terraform show -no-color terraform_plan.out > terraform_plan.log 2> >(tee terraform_plan.err >&2) && ret=0 || ret=$?
# Export the plan in json too
terraform show -json terraform_plan.out > terraform_plan.json
# Extract current state from the plan for later comparison
unzip terraform_plan.out tfstate
# Extract data from temp files and export them as outputs for next steps
# - changes made, if any
echo "changes<<terraform_verify_changes" >> $GITHUB_OUTPUT
awk '/the following actions/,0' terraform_plan.log >> $GITHUB_OUTPUT
echo "terraform_verify_changes" >> $GITHUB_OUTPUT
# - summary of the change, if any
echo "summary<<terraform_verify_summary" >> $GITHUB_OUTPUT
awk '/(Plan: |No changes. )/,1' terraform_plan.log | sed -e 's/Plan: /change(s): /' >> $GITHUB_OUTPUT
echo "terraform_verify_summary" >> $GITHUB_OUTPUT
# - stderr describing errors, if any
echo "stderr<<terraform_verify_stderr" >> $GITHUB_OUTPUT
cat terraform_plan.err >> $GITHUB_OUTPUT
echo "terraform_verify_stderr" >> $GITHUB_OUTPUT
# Exit with failure if any
exit $ret
- name: Comment
id: update
if: ${{ always() && github.event_name == 'pull_request' && inputs.auto_comment }}
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
// 1. Retrieve existing bot comments for the PR
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
})
const botComment = comments.find(comment => {
return comment.user.type === 'Bot' && comment.body.includes('pr-auto-comment-${{ inputs.tf_dir }}')
})
// 2. Prepare format of the comment, using toJSON to escape any special char
const changes = [
${{ toJSON(steps.verify.outputs.changes) }},
]
const errors = [
${{ toJSON(steps.fmt.outputs.stdout) }},
${{ toJSON(steps.init.outputs.stderr) }},
${{ toJSON(steps.validate.outputs.stderr) }},
${{ toJSON(steps.plan.outputs.stderr) }},
${{ toJSON(steps.verify.outputs.stderr) }},
]
const output = `
<!-- pr-auto-comment-${{ inputs.tf_dir }} -->
### ${{ github.workflow }}
| Step | Outcome |
| ---- | ------- |
| :pencil2: **Format** | \`${{ steps.fmt.outcome }}\` |
| :wrench: **Init** ️| \`${{ steps.init.outcome }}\` |
| :mag: **Validate** | \`${{ steps.validate.outcome }}\` |
| :page_facing_up: **Plan** | \`${{ steps.plan.outcome }}\` |
| :passport_control: **Verify** | \`${{ steps.verify.outcome }}\` |
| :point_right: **Result** | ${{ ( steps.plan.outcome == 'success' && steps.verify.outcome == 'success' && steps.verify.outputs.summary ) || 'with error(s) - see below' }} |
<details><summary>show change(s)</summary>
\`\`\`
${ changes.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
\`\`\`
</details>
<details><summary>show error(s)</summary>
\`\`\`
${ errors.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
\`\`\`
</details>
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*
*Workflow: \`${{ github.workflow_ref }}\`*`;
// 3. If we have a comment, update it, otherwise create a new one
const comment_data = {
owner: context.repo.owner,
repo: context.repo.repo,
body: output
}
if (botComment) {
comment_data.comment_id = botComment.id
github.rest.issues.updateComment(comment_data)
} else {
comment_data.issue_number = context.issue.number
github.rest.issues.createComment(comment_data)
}
- name: Apply
id: apply
if: ${{ inputs.apply_on_branch != '' && github.ref == format('refs/heads/{0}', inputs.apply_on_branch) }}
run: terraform apply -no-color -input=false -auto-approve terraform_plan.out
32 changes: 32 additions & 0 deletions .github/workflows/terraform_core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Workflow to continuously integrate Terraform plan for Tahoe-LAFS
# The deployment is not covered (yet), as it will require to auto commit
# both the `terraform.state` and `.terraform/terraform.state` files when
# apply_on_branch is enabled.

name: Terraform Core
concurrency: terraform_core_state

on:
push:
branches:
- main
paths:
- '.github/workflows/_terraform.yml'
- '.github/workflows/terraform_core.yml'
- 'terraform/core/**'
pull_request:
paths:
- '.github/workflows/_terraform.yml'
- '.github/workflows/terraform_core.yml'
- 'terraform/core/**'
workflow_dispatch:

jobs:
call-workflow-passing-data:
# Call the re-usable Terraform workflow
uses: ./.github/workflows/_terraform.yml
with:
tf_dir: terraform/core
gandi_url: 'https://api.sandbox.gandi.net'
secrets:
gandi_token: ${{ secrets.GANDI_TOKEN }}
30 changes: 30 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,33 @@ services:
limits:
cpus: '0.5'
memory: 512M

terraform-shell:
build:
context: docker/terraform
dockerfile: Dockerfile
args:
uid: "${_UID:-1000}"
gid: "${_GID:-1000}"
volumes:
- .:/var/lib/appdata
working_dir: /var/lib/appdata
environment:
# Required for Gandi resources
- TF_VAR_gandi_url=${GANDI_URL}
- TF_VAR_gandi_token=${GANDI_TOKEN}
# Required for Hetzner resources
- TF_VAR_hcloud_token=${HCLOUD_TOKEN}
entrypoint: /bin/sh
stdin_open: true
tty: true
hostname: terraform-shell.local
container_name: terraform-shell.local
network_mode: "bridge"
# Prevents container to hang the host
# Requires `... --compatibility run ...`
deploy:
resources:
limits:
cpus: '1.5'
memory: 256M
27 changes: 27 additions & 0 deletions docker/terraform/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
##############################
# General level requirements #
##############################

# Pull base image from official repo
FROM hashicorp/terraform:1.7.5@sha256:386b7bff108f9fd3b79a2e0110190c162b5e4aebf26afe3eef028fd89532c17e

##################################
# Application level requirements #
##################################

###########################
# User level requirements #
###########################

# Parameters for default user:group
ARG uid=1000
ARG user=appuser
ARG gid=1000
ARG group=appgroup

# Add user and group for build and runtime
RUN id "${user}" > /dev/null 2>&1 || \
{ addgroup -g "${gid}" "${group}" && adduser -D -h "/home/${user}" -s /bin/bash -G "${group}" -u "${uid}" "${user}"; }

# Switch to user
USER ${user}
1 change: 1 addition & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*/.terraform/*
2 changes: 2 additions & 0 deletions terraform/core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
terraform.tfstate.*
!.terraform/terraform.tfstate
22 changes: 22 additions & 0 deletions terraform/core/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions terraform/core/.terraform/terraform.tfstate
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": 3,
"serial": 1,
"lineage": "c2d32f6d-460b-75d4-0354-c71f999902f7",
"backend": {
"type": "local",
"config": {
"path": "./terraform.tfstate",
"workspace_dir": null
},
"hash": 3396623677
},
"modules": [
{
"path": [
"root"
],
"outputs": {},
"resources": {},
"depends_on": []
}
]
}
23 changes: 23 additions & 0 deletions terraform/core/dns_tl-org.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
resource "gandi_domain" "tl-org" {
name = "tahoe-lafs.org"
owner {
city = "Gotham"
country = "US"
data_obfuscated = true
email = "[email protected]"
extra_parameters = {
birth_city = ""
birth_country = ""
birth_date = ""
birth_department = ""
}
organisation = "Tahoe-LAFS"
family_name = "LAFS"
given_name = "Terlog"
mail_obfuscated = true
phone = "+31.234567890"
street_addr = "The place to be, 1"
type = "association"
zip = "12345"
}
}
13 changes: 13 additions & 0 deletions terraform/core/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# There is no state backend for the resources defined in this project
# It is only used to define the basic requirements used by the other
# ones in the parent folder.

# TODO: Assess if we could/should use Terraform Cloud only for this project,
# instead of committing the tfstate file.

# Lets make the local state explicit
terraform {
backend "local" {
path = "./terraform.tfstate"
}
}
Loading

0 comments on commit 5e118cb

Please sign in to comment.