Skip to content

Commit 5e118cb

Browse files
committed
Manage Gandi DNS with Terraform - sandbox only
Signed-off-by: Benoit Donneaux <benoit@leastauthority.com>
1 parent c33bd6d commit 5e118cb

12 files changed

+507
-0
lines changed

.github/workflows/_terraform.yml

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Re-usable workflow to continuously integrate and deploy Terraform plan
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
tf_version:
7+
description: 'Version of Terraform runtime to use'
8+
required: false
9+
type: string
10+
default: '1.7.4'
11+
tf_dir:
12+
description: 'Version of Terraform runtime to use'
13+
required: false
14+
type: string
15+
default: './'
16+
gh_runner_version:
17+
description: 'Version of the GitHub runner to use'
18+
required: false
19+
type: string
20+
default: 'ubuntu-24.04'
21+
auto_comment:
22+
description: 'Enable automatic comment on GitHub pull request'
23+
required: false
24+
type: boolean
25+
default: true
26+
apply_on_branch:
27+
description: 'Automaticaly apply plan when on a specific branch'
28+
required: false
29+
type: string
30+
default: ''
31+
gandi_url:
32+
description: 'Gandi API URL'
33+
required: false
34+
type: string
35+
default: 'https://api.gandi.net'
36+
secrets:
37+
gandi_token:
38+
description: 'API token for Gandi'
39+
required: false
40+
hcloud_token:
41+
description: 'API token for Hetzner Cloud'
42+
required: false
43+
44+
jobs:
45+
terraform:
46+
name: Terraform
47+
runs-on: ${{ inputs.gh_runner_version }}
48+
defaults:
49+
run:
50+
working-directory: ${{ inputs.tf_dir }}
51+
env:
52+
TF_VAR_gandi_token: ${{ secrets.gandi_token }}
53+
TF_VAR_hcloud_token: ${{ secrets.hcloud_token }}
54+
permissions:
55+
pull-requests: write
56+
contents: read
57+
58+
steps:
59+
- name: Checkout
60+
id: checkout
61+
uses: actions/checkout@v4
62+
63+
- name: Setup
64+
id: setup
65+
uses: hashicorp/setup-terraform@v3
66+
with:
67+
terraform_version: ${{ inputs.tf_version }}
68+
69+
- name: Format
70+
id: fmt
71+
run: terraform fmt -no-color -check -diff
72+
73+
- name: Init
74+
id: init
75+
run: terraform init -no-color -input=false
76+
77+
- name: Validate
78+
id: validate
79+
run: terraform validate -no-color
80+
81+
- name: Plan
82+
id: plan
83+
run: terraform plan -no-color -input=false -compact-warnings -out terraform_plan.out
84+
85+
- name: Post Setup
86+
id: post_setup
87+
# Only to avoid wrapper to mess up outputs in later steps
88+
uses: hashicorp/setup-terraform@v3
89+
with:
90+
terraform_version: ${{ inputs.tf_version }}
91+
terraform_wrapper: false
92+
93+
- name: Verify
94+
id: verify
95+
run: |
96+
# Process the plan to verify the presence of some data
97+
# which can be used later to make additional checks
98+
terraform show -no-color terraform_plan.out > terraform_plan.log 2> >(tee terraform_plan.err >&2) && ret=0 || ret=$?
99+
# Export the plan in json too
100+
terraform show -json terraform_plan.out > terraform_plan.json
101+
# Extract current state from the plan for later comparison
102+
unzip terraform_plan.out tfstate
103+
# Extract data from temp files and export them as outputs for next steps
104+
# - changes made, if any
105+
echo "changes<<terraform_verify_changes" >> $GITHUB_OUTPUT
106+
awk '/the following actions/,0' terraform_plan.log >> $GITHUB_OUTPUT
107+
echo "terraform_verify_changes" >> $GITHUB_OUTPUT
108+
# - summary of the change, if any
109+
echo "summary<<terraform_verify_summary" >> $GITHUB_OUTPUT
110+
awk '/(Plan: |No changes. )/,1' terraform_plan.log | sed -e 's/Plan: /change(s): /' >> $GITHUB_OUTPUT
111+
echo "terraform_verify_summary" >> $GITHUB_OUTPUT
112+
# - stderr describing errors, if any
113+
echo "stderr<<terraform_verify_stderr" >> $GITHUB_OUTPUT
114+
cat terraform_plan.err >> $GITHUB_OUTPUT
115+
echo "terraform_verify_stderr" >> $GITHUB_OUTPUT
116+
# Exit with failure if any
117+
exit $ret
118+
119+
- name: Comment
120+
id: update
121+
if: ${{ always() && github.event_name == 'pull_request' && inputs.auto_comment }}
122+
uses: actions/github-script@v7
123+
with:
124+
github-token: ${{ github.token }}
125+
script: |
126+
// 1. Retrieve existing bot comments for the PR
127+
const { data: comments } = await github.rest.issues.listComments({
128+
owner: context.repo.owner,
129+
repo: context.repo.repo,
130+
issue_number: context.issue.number,
131+
})
132+
const botComment = comments.find(comment => {
133+
return comment.user.type === 'Bot' && comment.body.includes('pr-auto-comment-${{ inputs.tf_dir }}')
134+
})
135+
136+
// 2. Prepare format of the comment, using toJSON to escape any special char
137+
const changes = [
138+
${{ toJSON(steps.verify.outputs.changes) }},
139+
]
140+
const errors = [
141+
${{ toJSON(steps.fmt.outputs.stdout) }},
142+
${{ toJSON(steps.init.outputs.stderr) }},
143+
${{ toJSON(steps.validate.outputs.stderr) }},
144+
${{ toJSON(steps.plan.outputs.stderr) }},
145+
${{ toJSON(steps.verify.outputs.stderr) }},
146+
]
147+
const output = `
148+
<!-- pr-auto-comment-${{ inputs.tf_dir }} -->
149+
### ${{ github.workflow }}
150+
| Step | Outcome |
151+
| ---- | ------- |
152+
| :pencil2: **Format** | \`${{ steps.fmt.outcome }}\` |
153+
| :wrench: **Init** ️| \`${{ steps.init.outcome }}\` |
154+
| :mag: **Validate** | \`${{ steps.validate.outcome }}\` |
155+
| :page_facing_up: **Plan** | \`${{ steps.plan.outcome }}\` |
156+
| :passport_control: **Verify** | \`${{ steps.verify.outcome }}\` |
157+
| :point_right: **Result** | ${{ ( steps.plan.outcome == 'success' && steps.verify.outcome == 'success' && steps.verify.outputs.summary ) || 'with error(s) - see below' }} |
158+
159+
<details><summary>show change(s)</summary>
160+
161+
\`\`\`
162+
${ changes.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
163+
\`\`\`
164+
165+
</details>
166+
167+
<details><summary>show error(s)</summary>
168+
169+
\`\`\`
170+
${ errors.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
171+
\`\`\`
172+
173+
</details>
174+
175+
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*
176+
*Workflow: \`${{ github.workflow_ref }}\`*`;
177+
178+
// 3. If we have a comment, update it, otherwise create a new one
179+
const comment_data = {
180+
owner: context.repo.owner,
181+
repo: context.repo.repo,
182+
body: output
183+
}
184+
if (botComment) {
185+
comment_data.comment_id = botComment.id
186+
github.rest.issues.updateComment(comment_data)
187+
} else {
188+
comment_data.issue_number = context.issue.number
189+
github.rest.issues.createComment(comment_data)
190+
}
191+
192+
- name: Apply
193+
id: apply
194+
if: ${{ inputs.apply_on_branch != '' && github.ref == format('refs/heads/{0}', inputs.apply_on_branch) }}
195+
run: terraform apply -no-color -input=false -auto-approve terraform_plan.out

.github/workflows/terraform_core.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Workflow to continuously integrate Terraform plan for Tahoe-LAFS
2+
# The deployment is not covered (yet), as it will require to auto commit
3+
# both the `terraform.state` and `.terraform/terraform.state` files when
4+
# apply_on_branch is enabled.
5+
6+
name: Terraform Core
7+
concurrency: terraform_core_state
8+
9+
on:
10+
push:
11+
branches:
12+
- main
13+
paths:
14+
- '.github/workflows/_terraform.yml'
15+
- '.github/workflows/terraform_core.yml'
16+
- 'terraform/core/**'
17+
pull_request:
18+
paths:
19+
- '.github/workflows/_terraform.yml'
20+
- '.github/workflows/terraform_core.yml'
21+
- 'terraform/core/**'
22+
workflow_dispatch:
23+
24+
jobs:
25+
call-workflow-passing-data:
26+
# Call the re-usable Terraform workflow
27+
uses: ./.github/workflows/_terraform.yml
28+
with:
29+
tf_dir: terraform/core
30+
gandi_url: 'https://api.sandbox.gandi.net'
31+
secrets:
32+
gandi_token: ${{ secrets.GANDI_TOKEN }}

docker-compose.yml

+30
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,33 @@ services:
9393
limits:
9494
cpus: '0.5'
9595
memory: 512M
96+
97+
terraform-shell:
98+
build:
99+
context: docker/terraform
100+
dockerfile: Dockerfile
101+
args:
102+
uid: "${_UID:-1000}"
103+
gid: "${_GID:-1000}"
104+
volumes:
105+
- .:/var/lib/appdata
106+
working_dir: /var/lib/appdata
107+
environment:
108+
# Required for Gandi resources
109+
- TF_VAR_gandi_url=${GANDI_URL}
110+
- TF_VAR_gandi_token=${GANDI_TOKEN}
111+
# Required for Hetzner resources
112+
- TF_VAR_hcloud_token=${HCLOUD_TOKEN}
113+
entrypoint: /bin/sh
114+
stdin_open: true
115+
tty: true
116+
hostname: terraform-shell.local
117+
container_name: terraform-shell.local
118+
network_mode: "bridge"
119+
# Prevents container to hang the host
120+
# Requires `... --compatibility run ...`
121+
deploy:
122+
resources:
123+
limits:
124+
cpus: '1.5'
125+
memory: 256M

docker/terraform/Dockerfile

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
##############################
2+
# General level requirements #
3+
##############################
4+
5+
# Pull base image from official repo
6+
FROM hashicorp/terraform:1.7.5@sha256:386b7bff108f9fd3b79a2e0110190c162b5e4aebf26afe3eef028fd89532c17e
7+
8+
##################################
9+
# Application level requirements #
10+
##################################
11+
12+
###########################
13+
# User level requirements #
14+
###########################
15+
16+
# Parameters for default user:group
17+
ARG uid=1000
18+
ARG user=appuser
19+
ARG gid=1000
20+
ARG group=appgroup
21+
22+
# Add user and group for build and runtime
23+
RUN id "${user}" > /dev/null 2>&1 || \
24+
{ addgroup -g "${gid}" "${group}" && adduser -D -h "/home/${user}" -s /bin/bash -G "${group}" -u "${uid}" "${user}"; }
25+
26+
# Switch to user
27+
USER ${user}

terraform/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*/.terraform/*

terraform/core/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
terraform.tfstate.*
2+
!.terraform/terraform.tfstate

terraform/core/.terraform.lock.hcl

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"version": 3,
3+
"serial": 1,
4+
"lineage": "c2d32f6d-460b-75d4-0354-c71f999902f7",
5+
"backend": {
6+
"type": "local",
7+
"config": {
8+
"path": "./terraform.tfstate",
9+
"workspace_dir": null
10+
},
11+
"hash": 3396623677
12+
},
13+
"modules": [
14+
{
15+
"path": [
16+
"root"
17+
],
18+
"outputs": {},
19+
"resources": {},
20+
"depends_on": []
21+
}
22+
]
23+
}

terraform/core/dns_tl-org.tf

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
resource "gandi_domain" "tl-org" {
2+
name = "tahoe-lafs.org"
3+
owner {
4+
city = "Gotham"
5+
country = "US"
6+
data_obfuscated = true
7+
email = "contact@example.com"
8+
extra_parameters = {
9+
birth_city = ""
10+
birth_country = ""
11+
birth_date = ""
12+
birth_department = ""
13+
}
14+
organisation = "Tahoe-LAFS"
15+
family_name = "LAFS"
16+
given_name = "Terlog"
17+
mail_obfuscated = true
18+
phone = "+31.234567890"
19+
street_addr = "The place to be, 1"
20+
type = "association"
21+
zip = "12345"
22+
}
23+
}

terraform/core/main.tf

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# There is no state backend for the resources defined in this project
2+
# It is only used to define the basic requirements used by the other
3+
# ones in the parent folder.
4+
5+
# TODO: Assess if we could/should use Terraform Cloud only for this project,
6+
# instead of committing the tfstate file.
7+
8+
# Lets make the local state explicit
9+
terraform {
10+
backend "local" {
11+
path = "./terraform.tfstate"
12+
}
13+
}

0 commit comments

Comments
 (0)