Merge pull request 'feat: initial sonarr terraform configuration' (#1) from feat/initial-config into main
ci/woodpecker/push/apply Pipeline was successful

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-06-29 23:36:10 +10:00
36 changed files with 852 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.terraform/
*.tfstate
*.tfstate.backup
*.tfplan
backend.tf
.terragrunt-cache/
+26
View File
@@ -0,0 +1,26 @@
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
exclude: ^modules/
- id: tflint
exclude: ^modules/
- 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",
]
+23
View File
@@ -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-sonarr
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+21
View File
@@ -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-sonarr
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+18
View File
@@ -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
+35
View File
@@ -0,0 +1,35 @@
.PHONY: init plan apply format
VAULT_AUTH_METHOD ?= approle
VAULT_K8S_ROLE ?= woodpecker_terraform_sonarr
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-sonarr) && \
export TF_VAR_sonarr_api_key=$$(vault kv get -field=apitoken kv/service/media-apps/sonarr)
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
+46
View File
@@ -0,0 +1,46 @@
locals {
config_files = fileset(".", "**/*.yaml")
all_configs = {
for file_path in local.config_files :
file_path => yamldecode(file(file_path))
}
config = {
custom_formats = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "custom_format/")
}
quality_profiles = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "quality_profile/")
}
download_clients = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "download_client/")
}
indexers = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "indexer/")
}
notifications = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "notification/")
}
delay_profiles = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "delay_profile/")
}
root_folders = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "root_folder/")
}
}
}
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: av1
implementation: ReleaseTitleSpecification
negate: false
required: false
value: av1
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: x264
implementation: ReleaseTitleSpecification
negate: false
required: false
value: (x|h)\.?264
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: x265
implementation: ReleaseTitleSpecification
negate: false
required: false
value: (((x|h)\.?265)|(HEVC))
+17
View File
@@ -0,0 +1,17 @@
include_custom_format_when_renaming: false
specifications:
- name: hdr10
implementation: ReleaseTitleSpecification
negate: false
required: true
value: hdr10
- name: x265
implementation: ReleaseTitleSpecification
negate: false
required: true
value: (((x|h)\.?265)|(HEVC))
- name: Surround Sound
implementation: ReleaseTitleSpecification
negate: false
required: true
value: DTS.?(HD|ES|X(?!\D))|TRUEHD|ATMOS|DD(\+|P).?([5-9])|EAC3.?([5-9])
+22
View File
@@ -0,0 +1,22 @@
include_custom_format_when_renaming: false
specifications:
- name: 10bit
implementation: ReleaseTitleSpecification
negate: false
required: true
value: 10bit
- name: hvec
implementation: ReleaseTitleSpecification
negate: false
required: false
value: hvec
- name: x265
implementation: ReleaseTitleSpecification
negate: false
required: true
value: (((x|h)\.?265)|(HEVC))
- name: 'release_iVy: iVy'
implementation: ReleaseGroupSpecification
negate: false
required: true
value: -iVy$
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: 'AsmoFuscated '
implementation: ReleaseGroupSpecification
negate: false
required: false
value: AsmoFuscated$
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: d3g
implementation: ReleaseGroupSpecification
negate: false
required: false
value: d3g$
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: iVy
implementation: ReleaseGroupSpecification
negate: false
required: false
value: iVy$
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: 1080p
implementation: ResolutionSpecification
negate: false
required: false
value: '1080'
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: 2160p
implementation: ResolutionSpecification
negate: false
required: false
value: '2160'
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: 720p
implementation: ResolutionSpecification
negate: false
required: false
value: '720'
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: bluray
implementation: SourceSpecification
negate: false
required: false
value: '6'
+7
View File
@@ -0,0 +1,7 @@
include_custom_format_when_renaming: false
specifications:
- name: hdtv
implementation: SourceSpecification
negate: false
required: false
value: '1'
+12
View File
@@ -0,0 +1,12 @@
include_custom_format_when_renaming: false
specifications:
- name: webdl
implementation: SourceSpecification
negate: false
required: false
value: '3'
- name: webrip
implementation: SourceSpecification
negate: false
required: false
value: '4'
+9
View File
@@ -0,0 +1,9 @@
enable_usenet: true
enable_torrent: true
preferred_protocol: usenet
usenet_delay: 0
torrent_delay: 0
bypass_if_highest_quality: true
bypass_if_above_custom_format_score: false
minimum_custom_format_score: 0
tags: []
+10
View File
@@ -0,0 +1,10 @@
enable: true
priority: 1
host: nzbget.service.consul
port: 443
use_ssl: true
username: svc_nzbsubmit
password: ""
tv_category: tvseries
remove_completed_downloads: true
remove_failed_downloads: true
+14
View File
@@ -0,0 +1,14 @@
enable_automatic_search: true
enable_interactive_search: true
enable_rss: true
priority: 25
base_url: "https://api.nzbgeek.info"
api_path: "/api"
api_key: ""
categories:
- 5040
- 5045
- 5060
- 5070
- 5080
anime_categories: []
+17
View File
@@ -0,0 +1,17 @@
name: "NZBgeek (Prowlarr)"
enable_automatic_search: true
enable_interactive_search: true
enable_rss: true
priority: 25
base_url: "https://prowlarr.service.consul/1/"
api_path: "/api"
api_key: ""
categories:
- 5000
- 5020
- 5030
- 5040
- 5045
- 5050
anime_categories:
- 5070
+16
View File
@@ -0,0 +1,16 @@
name: "Emby / Jellyfin"
host: jellyfin.service.consul
port: 443
use_ssl: true
api_key: ""
notify: false
update_library: true
on_grab: true
on_download: true
on_upgrade: true
on_rename: true
on_series_add: true
on_series_delete: true
on_episode_file_delete: true
on_episode_file_delete_for_upgrade: true
on_application_update: true
@@ -0,0 +1,99 @@
upgrade_allowed: true
cutoff: 7
cutoff_format_score: 5000
min_format_score: 0
quality_groups:
- qualities:
- id: 7
name: Bluray-1080p
source: bluray
resolution: 1080
- id: 1002
name: "WEB 1080p"
qualities:
- id: 15
name: WEBRip-1080p
source: webRip
resolution: 1080
- id: 3
name: WEBDL-1080p
source: web
resolution: 1080
- qualities:
- id: 6
name: Bluray-720p
source: bluray
resolution: 720
- id: 1001
name: "WEB 720p"
qualities:
- id: 14
name: WEBRip-720p
source: webRip
resolution: 720
- id: 5
name: WEBDL-720p
source: web
resolution: 720
- qualities:
- id: 9
name: HDTV-1080p
source: television
resolution: 1080
- qualities:
- id: 4
name: HDTV-720p
source: television
resolution: 720
format_items:
- name: release_AsmoFuscated
format: release_AsmoFuscated
score: 50
- name: release_d3g
format: release_d3g
score: 50
- name: release_iVy
format: release_iVy
score: 2000
- name: format_av1
format: format_av1
score: -1000
- name: size_0_800
format: size_0_800
score: 50
- name: source_hdtv
format: source_hdtv
score: 50
- name: source_webdl
format: source_webdl
score: 100
- name: source_bluray
format: source_bluray
score: 200
- name: size_3000_6000
format: size_3000_6000
score: 100
- name: size_1500_3000
format: size_1500_3000
score: 200
- name: size_800_1500
format: size_800_1500
score: 600
- name: hvec_10bit
format: hvec_10bit
score: 100
- name: resolution_2160p
format: resolution_2160p
score: -1000
- name: resolution_720p
format: resolution_720p
score: 50
- name: resolution_1080p
format: resolution_1080p
score: 500
- name: format_x264
format: format_x264
score: -500
- name: format_x265
format: format_x265
score: 1000
@@ -0,0 +1,123 @@
upgrade_allowed: true
cutoff: 19
cutoff_format_score: 5000
min_format_score: 0
quality_groups:
- qualities:
- id: 19
name: Bluray-2160p
source: bluray
resolution: 2160
- id: 1003
name: "WEB 2160p"
qualities:
- id: 17
name: WEBRip-2160p
source: webRip
resolution: 2160
- id: 18
name: WEBDL-2160p
source: web
resolution: 2160
- qualities:
- id: 16
name: HDTV-2160p
source: television
resolution: 2160
- qualities:
- id: 7
name: Bluray-1080p
source: bluray
resolution: 1080
- id: 1002
name: "WEB 1080p"
qualities:
- id: 15
name: WEBRip-1080p
source: webRip
resolution: 1080
- id: 3
name: WEBDL-1080p
source: web
resolution: 1080
- qualities:
- id: 6
name: Bluray-720p
source: bluray
resolution: 720
- id: 1001
name: "WEB 720p"
qualities:
- id: 14
name: WEBRip-720p
source: webRip
resolution: 720
- id: 5
name: WEBDL-720p
source: web
resolution: 720
- qualities:
- id: 9
name: HDTV-1080p
source: television
resolution: 1080
- qualities:
- id: 4
name: HDTV-720p
source: television
resolution: 720
format_items:
- name: hdr10
format: hdr10
score: 2000
- name: release_AsmoFuscated
format: release_AsmoFuscated
score: 600
- name: release_d3g
format: release_d3g
score: 1000
- name: release_iVy
format: release_iVy
score: 400
- name: format_av1
format: format_av1
score: -2000
- name: source_hdtv
format: source_hdtv
score: 50
- name: source_webdl
format: source_webdl
score: 100
- name: source_bluray
format: source_bluray
score: 200
- name: size_3000_6000
format: size_3000_6000
score: 300
- name: size_6000_10000
format: size_6000_10000
score: 400
- name: size_1500_3000
format: size_1500_3000
score: 200
- name: size_800_1500
format: size_800_1500
score: 100
- name: hvec_10bit
format: hvec_10bit
score: 500
- name: resolution_2160p
format: resolution_2160p
score: 500
- name: resolution_720p
format: resolution_720p
score: 50
- name: resolution_1080p
format: resolution_1080p
score: 150
- name: format_x265
format: format_x265
score: 1000
- name: format_x264
format: format_x264
score: -1000
+27
View File
@@ -0,0 +1,27 @@
upgrade_allowed: false
cutoff: 1
cutoff_format_score: 0
min_format_score: 0
quality_groups:
- qualities:
- id: 2
name: DVD
source: dvd
resolution: 480
- id: 1000
name: "WEB 480p"
qualities:
- id: 12
name: WEBRip-480p
source: webRip
resolution: 480
- id: 8
name: WEBDL-480p
source: web
resolution: 480
- qualities:
- id: 1
name: SDTV
source: television
resolution: 480
format_items: []
+1
View File
@@ -0,0 +1 @@
path: "/shared/media/tvseries"
+1
View File
@@ -0,0 +1 @@
export VAULT_ROLEID=3dd4f36f-222b-59e4-8f93-f7a39dec8cae
+32
View File
@@ -0,0 +1,32 @@
generate "backend" {
path = "backend.tf"
if_exists = "overwrite"
contents = <<EOF
provider "sonarr" {
url = "https://${path_relative_to_include()}"
api_key = var.sonarr_api_key
}
variable "sonarr_api_key" {
type = string
sensitive = true
}
terraform {
backend "consul" {
address = "https://consul.service.consul"
path = "infra/terraform/sonarr/${path_relative_to_include()}/state"
scheme = "https"
lock = true
ca_file = "/etc/pki/tls/certs/ca-bundle.crt"
}
required_version = ">= 1.10"
required_providers {
sonarr = {
source = "devopsarr/sonarr"
version = "3.4.2"
}
}
}
EOF
}
+25
View File
@@ -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/sonarr" {
version = "3.4.2"
constraints = "3.4.2"
hashes = [
"h1:2XcXWqATWjGrdkC4lngY4LvqUgwxfy5pjpLWRlNVYcY=",
"zh:01d675253aef5586b165bc4749d006f91cda6ce65b24842c7911cc178fe7e09d",
"zh:2da242ee58c1726cff9ce9260bc94756bc8a775a717da5aec8e9a8bf60578ac6",
"zh:30acc52a3a31ac75387728c27b3193f0419dcb72ee7488e8dbcd407884921205",
"zh:47adaba9c7915c832a9d8dfb5d0a18dd08b60fd7c531a810b2642ab75d200ee1",
"zh:551b580729cd82cc7a303d836985286e79a26062f02629bf51081ba2e4edf471",
"zh:5e5c5b1614cf0c61aa154543da1d5bda295a405fda52746d9193b0b8db922ddb",
"zh:65ebe76847129677f747364ee20591f01f5df4a67b06c5dff1bf53b813ec613a",
"zh:67db823f6016e345f2cbceb5300d0cce595bdeb0da32b15a1743724f0a4f978f",
"zh:75f119f674b3b0988133d38c1645e97cf6f0aee62a4131dfd62c8462636c2a94",
"zh:7ca3d6cfd4ccc2a9fb008ca93362c97a98f2ae98b55a56a7902fd33030d64179",
"zh:82033841c5a3fceecc1e3b84ef7328f027e2bb4bf63dfc24886510fed9d8a843",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:9aee54de9fcce6ef4f6b4f4f394a5628690d7d4849b03cd996ff991f8b29433a",
"zh:cda3fd66138de23856f869549aa06c82ce1f899474bc133d29d8463bfa032b16",
"zh:e4f8a67b8d08702056073a5ac8928dbb991767a52d3f36c3726399df0b5f2b96",
]
}
@@ -0,0 +1,27 @@
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/sonarr"
}
inputs = {
custom_formats = local.config.custom_formats
quality_profiles = local.config.quality_profiles
download_clients = local.config.download_clients
indexers = local.config.indexers
notifications = local.config.notifications
delay_profiles = local.config.delay_profiles
root_folders = local.config.root_folders
}
+114
View File
@@ -0,0 +1,114 @@
resource "sonarr_custom_format" "this" {
for_each = var.custom_formats
name = each.key
include_custom_format_when_renaming = lookup(each.value, "include_custom_format_when_renaming", false)
specifications = each.value.specifications
}
data "sonarr_custom_formats" "all" {}
locals {
custom_format_ids = merge(
{ for cf in data.sonarr_custom_formats.all.custom_formats : cf.name => cf.id },
{ for k, v in sonarr_custom_format.this : k => v.id },
)
}
resource "sonarr_quality_profile" "this" {
for_each = var.quality_profiles
name = each.key
upgrade_allowed = lookup(each.value, "upgrade_allowed", false)
cutoff = each.value.cutoff
cutoff_format_score = lookup(each.value, "cutoff_format_score", 0)
min_format_score = lookup(each.value, "min_format_score", 0)
min_upgrade_format_score = lookup(each.value, "min_upgrade_format_score", 0)
quality_groups = each.value.quality_groups
format_items = [
for fi in lookup(each.value, "format_items", []) : {
name = fi.name
format = local.custom_format_ids[fi.format]
score = fi.score
}
]
}
resource "sonarr_download_client_nzbget" "this" {
for_each = var.download_clients
name = lookup(each.value, "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", "")
tv_category = lookup(each.value, "tv_category", "")
remove_completed_downloads = lookup(each.value, "remove_completed_downloads", true)
remove_failed_downloads = lookup(each.value, "remove_failed_downloads", true)
lifecycle {
ignore_changes = [password]
}
}
resource "sonarr_indexer_newznab" "this" {
for_each = var.indexers
name = lookup(each.value, "name", each.key)
enable_automatic_search = lookup(each.value, "enable_automatic_search", true)
enable_interactive_search = lookup(each.value, "enable_interactive_search", true)
enable_rss = lookup(each.value, "enable_rss", true)
priority = lookup(each.value, "priority", 25)
base_url = each.value.base_url
api_path = lookup(each.value, "api_path", "/api")
api_key = lookup(each.value, "api_key", "")
categories = lookup(each.value, "categories", [])
anime_categories = lookup(each.value, "anime_categories", [])
lifecycle {
ignore_changes = [api_key]
}
}
resource "sonarr_notification_emby" "this" {
for_each = var.notifications
name = lookup(each.value, "name", each.key)
host = each.value.host
port = each.value.port
use_ssl = lookup(each.value, "use_ssl", false)
api_key = lookup(each.value, "api_key", "")
notify = lookup(each.value, "notify", false)
update_library = lookup(each.value, "update_library", true)
on_grab = lookup(each.value, "on_grab", true)
on_download = lookup(each.value, "on_download", true)
on_upgrade = lookup(each.value, "on_upgrade", true)
on_rename = lookup(each.value, "on_rename", true)
on_series_add = lookup(each.value, "on_series_add", true)
on_series_delete = lookup(each.value, "on_series_delete", true)
on_episode_file_delete = lookup(each.value, "on_episode_file_delete", true)
on_episode_file_delete_for_upgrade = lookup(each.value, "on_episode_file_delete_for_upgrade", true)
on_application_update = lookup(each.value, "on_application_update", true)
lifecycle {
ignore_changes = [api_key]
}
}
resource "sonarr_delay_profile" "this" {
for_each = var.delay_profiles
enable_usenet = lookup(each.value, "enable_usenet", true)
enable_torrent = lookup(each.value, "enable_torrent", true)
preferred_protocol = lookup(each.value, "preferred_protocol", "usenet")
usenet_delay = lookup(each.value, "usenet_delay", 0)
torrent_delay = lookup(each.value, "torrent_delay", 0)
bypass_if_highest_quality = lookup(each.value, "bypass_if_highest_quality", true)
bypass_if_above_custom_format_score = lookup(each.value, "bypass_if_above_custom_format_score", false)
minimum_custom_format_score = lookup(each.value, "minimum_custom_format_score", 0)
tags = each.value.tags
}
resource "sonarr_root_folder" "this" {
for_each = var.root_folders
path = each.value.path
}
+34
View File
@@ -0,0 +1,34 @@
variable "custom_formats" {
type = any
default = {}
}
variable "quality_profiles" {
type = any
default = {}
}
variable "download_clients" {
type = any
default = {}
}
variable "indexers" {
type = any
default = {}
}
variable "notifications" {
type = any
default = {}
}
variable "delay_profiles" {
type = any
default = {}
}
variable "root_folders" {
type = any
default = {}
}