Files
factory/iac/modules/cloudflare_token/main.tf
Gabriel Radureau 9b545e6f8f fix(iac): pin cloudflare provider + lockfile, trust homelab CA in gitea provider
With the runner CA fix (#11) the iac workflow now runs far enough to apply,
which exposed two provider problems:

cloudflare drift — `cloudflare/cloudflare` floated on `~> 5` with no committed
lock file, so CI pulled v5.21.1 where `cloudflare_account_token.policies[].resources`
is a JSON string, not a map ("Incorrect attribute value type"). Fix:
- pin to `~> 5.21` and commit a multi-platform `.terraform.lock.hcl`
  (linux_arm64 for the runner + darwin_arm64 for local);
- `jsonencode(...)` the module's policy resources;
- bind the cloudflare_token module to `cloudflare/cloudflare` explicitly (it was
  defaulting to `hashicorp/cloudflare`, pulling a redundant provider);
- stop `.gitignore` from hiding the lock file (the old `.terraform.*` rule did).

gitea provider TLS — it runs inside the dflook/terraform-apply container, which
doesn't trust the homelab CA (only the ubuntu-latest-ca runner does), so it
failed `x509: certificate signed by unknown authority` reaching
gitea.arcodange.lab. Fix: feed it the homelab CA via the provider's `cacert_file`
(TF_VAR_gitea_cacert_file -> the homelab.pem the workflow already materializes).

Validated locally with `tofu validate` + provider-schema inspection (no prod
calls). Complements #11. Out of scope (need a live run / operator): the OVH
consumer-key scope, and the R2 bucket "not found" on refresh (a state reconcile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 12:56:46 +02:00

82 lines
3.4 KiB
HCL

# Récupère toutes les permissions Cloudflare disponibles
data "cloudflare_account_api_token_permission_groups_list" "all" {
account_id = var.account_id
}
# Sélectionne uniquement les permissions demandées
locals {
# Simplifie le scope Cloudflare (ex: "account" depuis "com.cloudflare.api.account")
permission_map = {
for p in data.cloudflare_account_api_token_permission_groups_list.all.result :
"${split(".", p.scopes[0])[length(split(".", p.scopes[0])) - 1]}:${p.name}" => p.id
}
permission_map_from_id = zipmap(values(local.permission_map), keys(local.permission_map))
# Résout les permissions (si présentes) pour chaque catégorie
selected_account_permissions = var.permissions.account != null ? compact([
for name in var.permissions.account : lookup(local.permission_map, name, null)
]) : []
selected_bucket_permissions = var.bucket != null && try(var.permissions.bucket, null) != null ? compact([
for name in var.permissions.bucket : lookup(local.permission_map, name, null)
]) : []
# Validation des permissions introuvables
missing_permissions = concat(
[for name in coalesce(var.permissions.account, []) : name if lookup(local.permission_map, name, null) == null],
[for name in coalesce(var.permissions.bucket, []) : name if lookup(local.permission_map, name, null) == null]
)
# Ressources cibles
account_resource = {
"com.cloudflare.api.account.${var.account_id}" = "*"
}
bucket_resource = var.bucket != null ? {
"com.cloudflare.edge.r2.bucket.${var.account_id}_${var.bucket.jurisdiction}_${var.bucket.name}" = "*"
} : {}
# Policies construites dynamiquement
policies = [for policy in [
length(local.selected_account_permissions) > 0 ? {
effect = "allow"
permission_groups = [for id in local.selected_account_permissions : { id = id }]
resources = jsonencode(local.account_resource) # cloudflare provider >=5.20 types policies[].resources as a JSON string
} : null,
length(local.selected_bucket_permissions) > 0 ? {
effect = "allow"
permission_groups = [for id in local.selected_bucket_permissions : { id = id }]
resources = jsonencode(local.bucket_resource) # cloudflare provider >=5.20 types policies[].resources as a JSON string
} : null
] : policy if policy != null]
error_message = length(local.missing_permissions) > 0 ? format("Permissions introuvables : %s", join(", ", local.missing_permissions)) : ""
}
# Création du token
resource "cloudflare_account_token" "token" {
account_id = var.account_id
name = var.token_name
policies = local.policies
expires_on = null
lifecycle {
ignore_changes = [expires_on, policies] # ignore permission id change as unstable
replace_triggered_by = [null_resource.cloudflare_account_token_replace] # replace permission name change d
precondition {
condition = length(local.missing_permissions) == 0
error_message = local.error_message
}
}
}
resource "null_resource" "cloudflare_account_token_replace" { # replace token when permission names change
triggers = {
"account_permissions" = sha256(join("", sort([for p_id in local.selected_account_permissions : lookup(local.permission_map_from_id, p_id)])))
"bucket_permissions" = sha256(join("", sort([for p_id in local.selected_bucket_permissions : lookup(local.permission_map_from_id, p_id)])))
}
}