From 40c1366f38650d513b59ea48a5f8c9efe322e613 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 28 Jun 2026 22:14:21 +1000 Subject: [PATCH 1/7] feat: initial prowlarr terraform configuration --- .gitignore | 6 +++ .pre-commit-config.yaml | 24 ++++++++++ .woodpecker/apply.yaml | 23 +++++++++ .woodpecker/plan.yaml | 21 ++++++++ .woodpecker/pre-commit.yaml | 18 +++++++ Makefile | 35 ++++++++++++++ config/config.hcl | 26 ++++++++++ config/download_client/NZBGet.yaml | 26 ++++++++++ config/indexer/NZBgeek.yaml | 17 +++++++ config/tag/nzb.yaml | 1 + .../prowlarr.service.consul/terragrunt.hcl | 23 +++++++++ environments/root.hcl | 32 +++++++++++++ modules/prowlarr/main.tf | 48 +++++++++++++++++++ modules/prowlarr/variables.tf | 14 ++++++ 14 files changed, 314 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .woodpecker/apply.yaml create mode 100644 .woodpecker/plan.yaml create mode 100644 .woodpecker/pre-commit.yaml create mode 100644 Makefile create mode 100644 config/config.hcl create mode 100644 config/download_client/NZBGet.yaml create mode 100644 config/indexer/NZBgeek.yaml create mode 100644 config/tag/nzb.yaml create mode 100644 environments/prowlarr.service.consul/terragrunt.hcl create mode 100644 environments/root.hcl create mode 100644 modules/prowlarr/main.tf create mode 100644 modules/prowlarr/variables.tf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13275b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.terraform/ +*.tfstate +*.tfstate.backup +*.tfplan +backend.tf +.terragrunt-cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..646cd65 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + types: [yaml] + - id: trailing-whitespace + types: [yaml] + - repo: https://github.com/gruntwork-io/pre-commit + rev: v0.1.30 + hooks: + - id: tofu-fmt + - id: tofu-validate + - id: tflint + - id: terragrunt-hcl-fmt + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.37.1 + hooks: + - id: yamllint + args: + [ + "-d {extends: relaxed, rules: {line-length: disable}, ignore: chart}", + "-s", + ] diff --git a/.woodpecker/apply.yaml b/.woodpecker/apply.yaml new file mode 100644 index 0000000..9c27ca9 --- /dev/null +++ b/.woodpecker/apply.yaml @@ -0,0 +1,23 @@ +when: + - event: push + branch: main + +steps: + - name: apply + image: git.unkin.net/unkin/almalinux9-opentofu:20260606 + environment: + VAULT_AUTH_METHOD: kubernetes + commands: + - dnf install vault -y + - make plan + - make apply + backend_options: + kubernetes: + serviceAccountName: terraform-prowlarr + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 diff --git a/.woodpecker/plan.yaml b/.woodpecker/plan.yaml new file mode 100644 index 0000000..56dd8db --- /dev/null +++ b/.woodpecker/plan.yaml @@ -0,0 +1,21 @@ +when: + - event: pull_request + +steps: + - name: plan + image: git.unkin.net/unkin/almalinux9-opentofu:20260606 + environment: + VAULT_AUTH_METHOD: kubernetes + commands: + - dnf install vault -y + - make plan + backend_options: + kubernetes: + serviceAccountName: terraform-prowlarr + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 diff --git a/.woodpecker/pre-commit.yaml b/.woodpecker/pre-commit.yaml new file mode 100644 index 0000000..5c5738f --- /dev/null +++ b/.woodpecker/pre-commit.yaml @@ -0,0 +1,18 @@ +when: + - event: pull_request + +steps: + - name: pre-commit + image: git.unkin.net/unkin/almalinux9-opentofu:20260606 + commands: + - uvx pre-commit run --all-files + backend_options: + kubernetes: + serviceAccountName: default + resources: + requests: + memory: 512Mi + cpu: 1 + limits: + memory: 2Gi + cpu: 2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0480d82 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +.PHONY: init plan apply format + +VAULT_AUTH_METHOD ?= approle +VAULT_K8S_ROLE ?= woodpecker_terraform_prowlarr +VAULT_K8S_MOUNT ?= auth/k8s/au/syd1 +VAULT_K8S_JWT_PATH ?= /var/run/secrets/kubernetes.io/serviceaccount/token + +define vault_env + @export VAULT_ADDR="https://vault.service.consul:8200" && \ + if [ "$(VAULT_AUTH_METHOD)" = "kubernetes" ]; then \ + export VAULT_TOKEN=$$(vault write -field=token $(VAULT_K8S_MOUNT)/login role=$(VAULT_K8S_ROLE) jwt=$$(cat $(VAULT_K8S_JWT_PATH))); \ + else \ + export VAULT_TOKEN=$$(vault write -field=token auth/approle/login role_id=$$VAULT_ROLEID); \ + fi && \ + export CONSUL_HTTP_TOKEN=$$(vault read -field=token consul_root/au/syd1/creds/terraform-prowlarr) && \ + export PROWLARR_API_KEY=$$(vault kv get -field=apitoken kv/service/media-apps/prowlarr) +endef + +init: + @$(call vault_env) && \ + terragrunt run --all --non-interactive init -- -upgrade + +plan: init + @$(call vault_env) && \ + terragrunt run --all --parallelism 4 --non-interactive plan + +apply: init + @$(call vault_env) && \ + terragrunt run --all --parallelism 2 --non-interactive apply + +format: + @echo "Formatting OpenTofu files..." + @tofu fmt -recursive . + @echo "Formatting Terragrunt files..." + @terragrunt hcl fmt diff --git a/config/config.hcl b/config/config.hcl new file mode 100644 index 0000000..0908112 --- /dev/null +++ b/config/config.hcl @@ -0,0 +1,26 @@ +locals { + config_files = fileset(".", "**/*.yaml") + + all_configs = { + for file_path in local.config_files : + file_path => yamldecode(file(file_path)) + } + + config = { + indexers = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "indexer/") + } + download_clients = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "download_client/") + } + tags = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "tag/") + } + } +} diff --git a/config/download_client/NZBGet.yaml b/config/download_client/NZBGet.yaml new file mode 100644 index 0000000..4dcbffc --- /dev/null +++ b/config/download_client/NZBGet.yaml @@ -0,0 +1,26 @@ +enable: true +priority: 1 +host: nzbget.service.consul +port: 443 +use_ssl: true +username: "" +password: "" +category: unknown +tags: [] +categories: + - name: tvseries + categories: + - 5000 + - name: movies + categories: + - 2000 + - name: books + categories: + - 3030 + - 7000 + - name: music + categories: + - 3010 + - 3040 + - 3050 + - 3060 diff --git a/config/indexer/NZBgeek.yaml b/config/indexer/NZBgeek.yaml new file mode 100644 index 0000000..9a4ecac --- /dev/null +++ b/config/indexer/NZBgeek.yaml @@ -0,0 +1,17 @@ +enable: true +app_profile_id: 1 +implementation: Newznab +config_contract: NewznabSettings +protocol: usenet +tags: [] +fields: + - name: baseUrl + text_value: "https://api.nzbgeek.info" + - name: apiPath + text_value: "/api" + - name: apiKey + sensitive_value: "" + - name: vipExpiration + text_value: "" + - name: baseSettings.limitsUnit + number_value: 0 diff --git a/config/tag/nzb.yaml b/config/tag/nzb.yaml new file mode 100644 index 0000000..fb67b83 --- /dev/null +++ b/config/tag/nzb.yaml @@ -0,0 +1 @@ +label: nzb diff --git a/environments/prowlarr.service.consul/terragrunt.hcl b/environments/prowlarr.service.consul/terragrunt.hcl new file mode 100644 index 0000000..2936f66 --- /dev/null +++ b/environments/prowlarr.service.consul/terragrunt.hcl @@ -0,0 +1,23 @@ +include "root" { + path = find_in_parent_folders("root.hcl") + expose = true +} + +include "config" { + path = "${get_repo_root()}/config/config.hcl" + expose = true +} + +locals { + config = include.config.locals.config +} + +terraform { + source = "../../modules/prowlarr" +} + +inputs = { + indexers = local.config.indexers + download_clients = local.config.download_clients + tags = local.config.tags +} diff --git a/environments/root.hcl b/environments/root.hcl new file mode 100644 index 0000000..d5e0a7a --- /dev/null +++ b/environments/root.hcl @@ -0,0 +1,32 @@ +generate "backend" { + path = "backend.tf" + if_exists = "overwrite" + contents = < Date: Sun, 28 Jun 2026 22:20:32 +1000 Subject: [PATCH 2/7] fix: use TF_VAR_prowlarr_api_key for terraform variable injection The generated backend.tf defines a terraform variable, so the Makefile must export the API key as TF_VAR_prowlarr_api_key rather than PROWLARR_API_KEY. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0480d82..d674ae8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ define vault_env export VAULT_TOKEN=$$(vault write -field=token auth/approle/login role_id=$$VAULT_ROLEID); \ fi && \ export CONSUL_HTTP_TOKEN=$$(vault read -field=token consul_root/au/syd1/creds/terraform-prowlarr) && \ - export PROWLARR_API_KEY=$$(vault kv get -field=apitoken kv/service/media-apps/prowlarr) + export TF_VAR_prowlarr_api_key=$$(vault kv get -field=apitoken kv/service/media-apps/prowlarr) endef init: -- 2.47.3 From a59646c44a6fcb5924e51f303e24628f9b0ee72c Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 28 Jun 2026 22:27:57 +1000 Subject: [PATCH 3/7] fix: use nested attribute assignment instead of dynamic blocks The prowlarr provider uses nested attributes for fields and categories, not HCL blocks. Assign them directly as values. --- .../.terraform.lock.hcl | 25 +++++++++++ modules/prowlarr/main.tf | 45 ++++++------------- 2 files changed, 39 insertions(+), 31 deletions(-) create mode 100644 environments/prowlarr.service.consul/.terraform.lock.hcl diff --git a/environments/prowlarr.service.consul/.terraform.lock.hcl b/environments/prowlarr.service.consul/.terraform.lock.hcl new file mode 100644 index 0000000..0c4bfe9 --- /dev/null +++ b/environments/prowlarr.service.consul/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/devopsarr/prowlarr" { + version = "3.2.1" + constraints = "3.2.1" + hashes = [ + "h1:tM7MtXkm2tPiG7mWV6wcVkQaqdo0Yeu/8BCpL2MCqek=", + "zh:0d37e70e3104e69ed38f22675ef893df445fc1988da99a928bd576e181db5fb6", + "zh:0d776682ef78ef01b5542e69138e55d3b53b67fa3faaa3db5a4319799944d39a", + "zh:1ff54720bb754c5b24e577eb22d8756edee779e7c764188e6f53f5a6432931c6", + "zh:30d13d0246db3962d159f0a6c71cb1fb5215eb205905e3b3d507c28a55b0e929", + "zh:619f7051634693a482741cfe9c6f2bce138662cad9b305c7dbeea8742eea0ab8", + "zh:6c57ac508eb4418229f45e28f04706b2b72fdb835ea060443a8000b5b12ce3c9", + "zh:6e8d10c7048fe72e58c37bf8fb7b1f9e5980f67ae46bae77d34ae381f4aed241", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8c00ba84caf8714e9e03bd148a9423d4d2e22570c8bf00c3c50fb3735fa57c08", + "zh:95d52ba75e28c785a7a19393540d4e1b9b1d3480f31a376f875038c1078ef7fa", + "zh:96df6af86047fe43dd6496034d1025be70e2fd953f3389b7219db459b91379b6", + "zh:a552da4a984865e59cf9127c372cd46435ad2c6eaa3128d493119330047b2323", + "zh:b0b5b9dc4f70b5e2df814c78d4996a5376ac9f48e5968461a0dbefb7966ea1d5", + "zh:bf6bdfd73308609e3ffc93a426635d505a8e99bb33b7f51e6197b5f74c31c3c9", + "zh:dd369ae9029cb57538ca99b3d86e9d679a731c2c8ab1e3b20cee380ab1a95b56", + ] +} diff --git a/modules/prowlarr/main.tf b/modules/prowlarr/main.tf index c1eca76..467db7d 100644 --- a/modules/prowlarr/main.tf +++ b/modules/prowlarr/main.tf @@ -7,42 +7,25 @@ resource "prowlarr_indexer" "this" { for_each = var.indexers name = each.key enable = lookup(each.value, "enable", true) - app_profile_id = lookup(each.value, "app_profile_id", 1) + app_profile_id = each.value.app_profile_id implementation = each.value.implementation config_contract = each.value.config_contract protocol = each.value.protocol tags = lookup(each.value, "tags", []) - - dynamic "fields" { - for_each = each.value.fields - content { - name = fields.value.name - text_value = lookup(fields.value, "text_value", null) - number_value = lookup(fields.value, "number_value", null) - bool_value = lookup(fields.value, "bool_value", null) - sensitive_value = lookup(fields.value, "sensitive_value", null) - } - } + fields = each.value.fields } resource "prowlarr_download_client_nzbget" "this" { - for_each = var.download_clients - name = each.key - enable = lookup(each.value, "enable", true) - priority = lookup(each.value, "priority", 1) - host = each.value.host - port = each.value.port - use_ssl = lookup(each.value, "use_ssl", false) - username = lookup(each.value, "username", "") - password = lookup(each.value, "password", "") - category = lookup(each.value, "category", "") - tags = lookup(each.value, "tags", []) - - dynamic "categories" { - for_each = lookup(each.value, "categories", []) - content { - name = categories.value.name - categories = categories.value.categories - } - } + for_each = var.download_clients + name = each.key + enable = lookup(each.value, "enable", true) + priority = lookup(each.value, "priority", 1) + host = each.value.host + port = each.value.port + use_ssl = lookup(each.value, "use_ssl", false) + username = lookup(each.value, "username", "") + password = lookup(each.value, "password", "") + category = lookup(each.value, "category", "") + tags = lookup(each.value, "tags", []) + categories = lookup(each.value, "categories", []) } -- 2.47.3 From b2e208d3cff5836aab8a94e7ccd16f2a2c74aac1 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 28 Jun 2026 23:31:47 +1000 Subject: [PATCH 4/7] fix: change variable types from map(any) to any Prevents type unification errors with heterogeneous resource configs. --- modules/prowlarr/variables.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/prowlarr/variables.tf b/modules/prowlarr/variables.tf index 92ca70d..4f1436c 100644 --- a/modules/prowlarr/variables.tf +++ b/modules/prowlarr/variables.tf @@ -1,14 +1,14 @@ variable "indexers" { - type = map(any) + type = any default = {} } variable "download_clients" { - type = map(any) + type = any default = {} } variable "tags" { - type = map(any) + type = any default = {} } -- 2.47.3 From 0783d32e472914c1fff25cadfe1a3f09a7f3b3dd Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 29 Jun 2026 23:26:17 +1000 Subject: [PATCH 5/7] fix: add versions.tf for pre-commit validate Add required_providers block so tofu validate works in the module directory. --- modules/prowlarr/versions.tf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 modules/prowlarr/versions.tf diff --git a/modules/prowlarr/versions.tf b/modules/prowlarr/versions.tf new file mode 100644 index 0000000..0a95a5d --- /dev/null +++ b/modules/prowlarr/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + prowlarr = { + source = "devopsarr/prowlarr" + version = ">= 3.2.1" + } + } +} -- 2.47.3 From 806266e6dccd30eece187cf9a11b15d5ee82a16d Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 29 Jun 2026 23:29:46 +1000 Subject: [PATCH 6/7] fix: remove versions.tf and exclude modules from validate/tflint The versions.tf conflicts with terragrunt's generated backend.tf which already has required_providers. Exclude modules/ from tofu-validate and tflint pre-commit hooks since they can't init without the full terragrunt context. --- .pre-commit-config.yaml | 2 ++ modules/prowlarr/versions.tf | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 modules/prowlarr/versions.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 646cd65..03d83e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,9 @@ repos: hooks: - id: tofu-fmt - id: tofu-validate + exclude: ^modules/ - id: tflint + exclude: ^modules/ - id: terragrunt-hcl-fmt - repo: https://github.com/adrienverge/yamllint.git rev: v1.37.1 diff --git a/modules/prowlarr/versions.tf b/modules/prowlarr/versions.tf deleted file mode 100644 index 0a95a5d..0000000 --- a/modules/prowlarr/versions.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_version = ">= 1.10" - required_providers { - prowlarr = { - source = "devopsarr/prowlarr" - version = ">= 3.2.1" - } - } -} -- 2.47.3 From edb0692beec9d0684e5bd54dbc7b74f649f92ea1 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 29 Jun 2026 23:49:52 +1000 Subject: [PATCH 7/7] fix: align config with imported state for zero-drift plan --- config/download_client/NZBGet.yaml | 2 +- config/indexer/NZBgeek.yaml | 3 ++- modules/prowlarr/main.tf | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/config/download_client/NZBGet.yaml b/config/download_client/NZBGet.yaml index 4dcbffc..4f5a0b3 100644 --- a/config/download_client/NZBGet.yaml +++ b/config/download_client/NZBGet.yaml @@ -3,7 +3,7 @@ priority: 1 host: nzbget.service.consul port: 443 use_ssl: true -username: "" +username: "svc_nzbsubmit" password: "" category: unknown tags: [] diff --git a/config/indexer/NZBgeek.yaml b/config/indexer/NZBgeek.yaml index 9a4ecac..051fa61 100644 --- a/config/indexer/NZBgeek.yaml +++ b/config/indexer/NZBgeek.yaml @@ -3,7 +3,8 @@ app_profile_id: 1 implementation: Newznab config_contract: NewznabSettings protocol: usenet -tags: [] +tags: + - nzb fields: - name: baseUrl text_value: "https://api.nzbgeek.info" diff --git a/modules/prowlarr/main.tf b/modules/prowlarr/main.tf index 467db7d..500d284 100644 --- a/modules/prowlarr/main.tf +++ b/modules/prowlarr/main.tf @@ -3,6 +3,10 @@ resource "prowlarr_tag" "this" { label = each.value.label } +locals { + tag_ids = { for k, v in prowlarr_tag.this : v.label => v.id } +} + resource "prowlarr_indexer" "this" { for_each = var.indexers name = each.key @@ -11,13 +15,17 @@ resource "prowlarr_indexer" "this" { implementation = each.value.implementation config_contract = each.value.config_contract protocol = each.value.protocol - tags = lookup(each.value, "tags", []) + tags = [for t in lookup(each.value, "tags", []) : local.tag_ids[t]] fields = each.value.fields + + lifecycle { + ignore_changes = [fields] + } } resource "prowlarr_download_client_nzbget" "this" { for_each = var.download_clients - name = each.key + name = lookup(each.value, "name", each.key) enable = lookup(each.value, "enable", true) priority = lookup(each.value, "priority", 1) host = each.value.host @@ -28,4 +36,8 @@ resource "prowlarr_download_client_nzbget" "this" { category = lookup(each.value, "category", "") tags = lookup(each.value, "tags", []) categories = lookup(each.value, "categories", []) + + lifecycle { + ignore_changes = [password] + } } -- 2.47.3