diff --git a/ReadMe.md b/ReadMe.md
index d5a394e..84f38b2 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -111,12 +111,12 @@ Static secrets, such as API tokens and passwords, are stored as **sensitive vari
> **Note**: Some non-secret values (like `public_facing_ip`) are marked as sensitive to prevent them from being exposed in public logs or plan outputs.
-### Dynamic Secrets
+'''### Dynamic Secrets
For services deployed by the `proxy_service_stack` module, credentials are not stored statically. Instead:
-1. **Generated Secrets**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will generate a unique, high-entropy value for each and inject it into the container's environment variables.
+1. **Infisical Integration**: For services requiring arbitrary secrets (like API keys or JWT signing keys), you can define a list of secret names in the stack's YAML file under `generated_secrets`. Terraform will fetch these secrets from Infisical Cloud and inject them into the container's environment variables.
2. **OAuth Credentials**: For services using OAuth, the `docker-stack` module automatically creates an OAuth2 provider in Authentik. The resulting `client_id` and `client_secret` are then injected as environment variables into the container.
-This ensures that secrets are managed dynamically and securely, with minimal manual intervention.
+This ensures that secrets are managed dynamically and securely, with minimal manual intervention.'''
## ⚠️ Operational Notes
diff --git a/config/services/authentik.yaml b/config/services/authentik.yaml
deleted file mode 100644
index ef00236..0000000
--- a/config/services/authentik.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-authentik:
- admin-user: ""
\ No newline at end of file
diff --git a/config/services/deluge-vpn.yaml b/config/services/deluge-vpn.yaml
index a2a9be7..7adfe82 100644
--- a/config/services/deluge-vpn.yaml
+++ b/config/services/deluge-vpn.yaml
@@ -1,6 +1,6 @@
deluge-vpn:
dns:
- enabled: true
+ enabled: false
network:
internal: true
service_port: 8112
@@ -8,18 +8,19 @@ deluge-vpn:
- name: "br1"
ip_address: "192.168.5.28"
auth:
- enabled: true
- proxy: true
+ enabled: false
group: "Arr"
+ secrets:
+ VPN_USER: "VPN_USER"
+ VPN_PASS: "VPN_PASS"
icon: "https://vectorified.com/images/deluge-icon-3.jpg"
service_name: "deluge-vpn"
image_name: "binhex/arch-delugevpn:2.2"
- domain_name: "prowlarr.dcapi.app"
mounts:
- "/etc/localtime:/etc/localtime:ro"
- "/mnt/user/Arr/deluge-data:/config"
- "/mnt/user/Downloads:/data/downloads"
- - "/mnt/user/appdata/binhex-delugevpn/openvpn:/config/openvpn"
+ - "/mnt/user/Arr/deluge-data/openvpn:/config/openvpn"
capabilities:
add:
- "CAP_NET_ADMIN"
@@ -34,4 +35,5 @@ deluge-vpn:
- "VPN_ENABLED=yes"
- "VPN_PROV=custom"
- "VPN_CLIENT=openvpn"
- - "LAN_NETWORK=192.168.1.0/24"
\ No newline at end of file
+ - "LAN_NETWORK=192.168.1.0/24"
+ - "UPDATE_VAL=1"
\ No newline at end of file
diff --git a/config/services/flaresolverr.yaml b/config/services/flaresolverr.yaml
index c60189a..73b0ed1 100644
--- a/config/services/flaresolverr.yaml
+++ b/config/services/flaresolverr.yaml
@@ -1,11 +1,13 @@
flaresolverr:
- service_name: "flaresolverr"
- image_name: "flaresolverr/flaresolverr:v3.3.25"
+ dns:
+ enabled: false
network:
internal: true
service_port: 8112
networks:
- name: "br1"
- ip_address: "192.168.5.27"
- env:
- - "TZ=America/Chicago"
+ ip_address: "192.168.5.29"
+ auth:
+ enabled: false
+ service_name: "flaresolverr"
+ image_name: "flaresolverr/flaresolverr:v3.3.25"
\ No newline at end of file
diff --git a/config/services/requestrr.yaml b/config/services/requestrr.yaml
index 0543fbf..1310436 100644
--- a/config/services/requestrr.yaml
+++ b/config/services/requestrr.yaml
@@ -1,12 +1,16 @@
requestrr:
- service_name: "requestrr"
- image_name: "linuxserver/requestrr:2.1.2"
+ dns:
+ enabled: false
network:
internal: true
service_port: 4545
networks:
- name: "br1"
ip_address: "192.168.5.27"
+ auth:
+ enabled: false
+ service_name: "requestrr"
+ image_name: "linuxserver/requestrr:2.1.2"
mounts:
- "/mnt/user/Arr/requestrr-data:/config"
- "/etc/localtime:/etc/localtime:ro"
\ No newline at end of file
diff --git a/config/stacks/arr.yaml b/config/stacks/arr.yaml
index 51df0f9..5a50c25 100644
--- a/config/stacks/arr.yaml
+++ b/config/stacks/arr.yaml
@@ -21,7 +21,10 @@
ip_address: "192.168.5.22"
auth:
enabled: true
- proxy: true
+ proxy:
+ enabled: true
+ user_secret: "prowlarr_username"
+ pass_secret: "prowlarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/prowlarr-icon-512x512-v9ekdjxx.png"
service_name: "prowlarr"
@@ -41,7 +44,10 @@
ip_address: "192.168.5.23"
auth:
enabled: true
- proxy: true
+ proxy:
+ enabled: true
+ user_secret: "sonarr_username"
+ pass_secret: "sonarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/sonarr-icon-1024x1024-wkay604k.png"
service_name: "sonarr"
@@ -49,6 +55,8 @@
domain_name: "sonarr.dcapi.app"
mounts:
- "/mnt/user/Arr/sonarr-data:/config"
+ - "/mnt/user/Media:/media"
+ - "/mnt/user/Downloads:/downloads"
radarr:
dns:
enabled: true
@@ -61,7 +69,10 @@
ip_address: "192.168.5.24"
auth:
enabled: true
- proxy: true
+ proxy:
+ enabled: true
+ user_secret: "radarr_username"
+ pass_secret: "radarr_password"
group: "Arr"
icon: "https://static-00.iconduck.com/assets.00/radarr-icon-462x512-bydv4e4f.png"
service_name: "radarr"
@@ -69,6 +80,8 @@
domain_name: "radarr.dcapi.app"
mounts:
- "/mnt/user/Arr/radarr-data:/config"
+ - "/mnt/user/Media:/media"
+ - "/mnt/user/Downloads:/downloads"
lidarr:
dns:
enabled: true
@@ -81,9 +94,14 @@
ip_address: "192.168.5.25"
auth:
enabled: true
- proxy: true
+ proxy:
+ enabled: true
+ user_secret: "lidarr_username"
+ pass_secret: "lidarr_password"
group: "Arr"
service_name: "lidarr"
image_name: "linuxserver/lidarr:2.13.0-develop"
mounts:
- - "/mnt/user/Arr/lidarr-data:/config"
\ No newline at end of file
+ - "/mnt/user/Arr/lidarr-data:/config"
+ - "/mnt/user/Media:/media"
+ - "/mnt/user/Downloads:/downloads"
\ No newline at end of file
diff --git a/config/system.yaml b/config/system.yaml
index f1a39be..b6c595a 100644
--- a/config/system.yaml
+++ b/config/system.yaml
@@ -8,4 +8,8 @@ zones:
admin_username: "DCCoder"
existing_networks:
- "br1"
- - "br0"
\ No newline at end of file
+ - "br0"
+infisical:
+ project: "home-net-ln-sy"
+ environment: "dev"
+ folder: "/secrets"
\ No newline at end of file
diff --git a/docs/service-config.md b/docs/service-config.md
index c0b05db..8169d75 100644
--- a/docs/service-config.md
+++ b/docs/service-config.md
@@ -13,7 +13,23 @@ The structure of each top-level key in these files is **not standardized**. It i
Below are examples of different structures found in this project.
-### Example 1: A Standalone Docker Service (`deluge-vpn`)
+'''### Example 1: A Standalone Docker Service (`flaresolverr`)
+
+This service is consumed by the `flaresolverr_service` module in `services.tf`, which is a wrapper around the generic `docker` module.
+
+```yaml
+flaresolverr:
+ service_name: "flaresolverr"
+ image_name: "flaresolverr/flaresolverr:latest"
+ network:
+ networks:
+ - "br1"
+ env:
+ - "LOG_LEVEL=info"
+ - "TZ=America/New_York"
+```
+
+### Example 2: A Standalone Docker Service (`deluge-vpn`)
This service is consumed by the `delugevpn_service` module in `services.tf`, which is a wrapper around the generic `docker` module.
@@ -31,7 +47,7 @@ deluge-vpn:
capabilities:
add:
- "NET_ADMIN"
-```
+```''
### Example 2: A Global Configuration Object (`authentik`)
diff --git a/docs/stack-config.md b/docs/stack-config.md
index c5d56c8..57a1b29 100644
--- a/docs/stack-config.md
+++ b/docs/stack-config.md
@@ -143,11 +143,3 @@ your_stack_name:
* **`auth.oauth.redirect_uris`**: A list of additional relative paths (e.g., `/oauth/callback`) that will be appended to the service's domain name to form the complete, valid OAuth redirect URIs required by Authentik.
-## Generated Secrets Workflow
-
-* **`generated_secrets` (Stack Level)**: A list of string names (e.g., `"API_KEY"`) for secrets that your services will consume.
- * **Workflow**:
- 1. **Definition**: You must first define these secret names in the `config/secrets.yaml` file.
- 2. **Generation & Storage**: The `generated_secrets` Terraform module (run as part of the root module) reads `config/secrets.yaml`, generates a unique, value for each listed secret, and then securely stores these generated values in Infisical Cloud.
- 3. **Consumption by Stacks**: When you list a secret name here, the `docker-stack` module will automatically pull the corresponding secret value from Infisical and inject it as an environment variable into the container.
- * **Important Note**: Any secret name listed in a stack's `generated_secrets` field *must* first be defined in `config/secrets.yaml` to ensure it is generated and stored in Infisical. If a requested secret is not found, the Terraform plan will fail, preventing deployment with missing credentials.
\ No newline at end of file
diff --git a/terraform/modules/docker-service/auth.tf b/terraform/modules/docker-service/auth.tf
new file mode 100644
index 0000000..28dfac1
--- /dev/null
+++ b/terraform/modules/docker-service/auth.tf
@@ -0,0 +1,50 @@
+
+
+module "proxy_authentication" {
+ source = "../proxy_auth"
+ count = var.service.auth.enabled && var.service.auth.proxy.enabled ? 1 : 0
+
+ group = var.service.auth.group
+ description = var.service.description
+ # If a static IP is defined, use it. Otherwise, fall back to the service name,
+ # which is resolvable within a Docker network.
+ internal_host = "http://${coalesce(local.service_ip_address, var.service.service_name)}:${var.service.network.service_port}"
+ external_host = var.service.dns.domain_name
+ name = var.service.service_name
+ username_attribute = "${var.service.service_name}_username"
+ password_attribute = "${var.service.service_name}_password"
+ create_access_group = true
+ access_group_name = "tf_${var.service.service_name}"
+ user_to_add_to_access_group = var.system.network_admin_username
+ access_group_attributes = jsonencode(
+ {
+ "${var.service.service_name}_username" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.user_secret, null), "${var.service.service_name}_username")].value,
+ "${var.service.service_name}_password" : data.infisical_secrets.secrets[0].secrets[coalesce(try(var.service.auth.proxy.pass_secret, null), "${var.service.service_name}_password")].value
+ }
+ )
+}
+
+module "oauth_authentication" {
+ source = "../oauth_auth"
+ count = var.service.auth.enabled && var.service.auth.oauth.enabled ? 1 : 0
+
+ group = var.service.auth.group
+ description = var.service.description
+ name = var.service.service_name
+ create_access_group = true
+ access_group_name = "tf_${var.service.service_name}"
+ user_to_add_to_access_group = var.system.network_admin_username
+ allowed_redirect_uris = concat(
+ [
+ {
+ matching_mode = "strict",
+ url = "https://${var.service.dns.domain_name}" }
+ ],
+ [
+ for uri_path in coalesce(var.service.auth.oauth.redirect_uris, []) : {
+ matching_mode = "strict",
+ url = "https://${var.service.dns.domain_name}/${uri_path}"
+ }
+ ]
+ )
+}
diff --git a/terraform/modules/docker-service/dns.tf b/terraform/modules/docker-service/dns.tf
new file mode 100644
index 0000000..0efdf2f
--- /dev/null
+++ b/terraform/modules/docker-service/dns.tf
@@ -0,0 +1,21 @@
+data "nginxproxymanager_access_lists" "access_lists" {}
+
+module "service_dns" {
+ source = "../dns"
+ count = var.service.dns.enabled ? 1 : 0
+
+ internal_only = var.service.network.internal
+ service_port = var.service.auth.proxy.enabled ? var.system.authentik.port : var.service.network.service_port
+ zone_name = var.zone_name
+ domain_name = var.service.dns.domain_name
+
+ # Really don't like having the ACLs hardcoded here...
+ access_list_id = var.service.network.internal ? local.npm_access_lists_by_name["Internal Only"] : local.npm_access_lists_by_name["Cloudflare"]
+ internal_host_ipv4 = var.system.proxy_ip
+ # If not using proxy auth, point to the service's static IP. If no static IP,
+ # fall back to the service name, which NPM can use as a hostname.
+ service_ipv4 = var.service.auth.proxy.enabled ? var.system.authentik.ip_address : coalesce(local.service_ip_address, var.service.service_name)
+ admin_email = var.system.network_admin_email
+ dns_cloudflare_api_token = var.system.cloudflare_api_token
+ external_host_ipv4 = var.system.public_facing_ip
+}
\ No newline at end of file
diff --git a/terraform/modules/docker-service/locals.tf b/terraform/modules/docker-service/locals.tf
new file mode 100644
index 0000000..61d64a1
--- /dev/null
+++ b/terraform/modules/docker-service/locals.tf
@@ -0,0 +1,25 @@
+locals {
+ # Determine the primary IP address for the service from its network configuration.
+ service_ip_address = try(
+ try([for n in var.service.network.networks : n.ip_address if n.name == "br1" && n.ip_address != null][0], null),
+ try([for n in var.service.network.networks : n.ip_address if n.name == "br0" && n.ip_address != null][0], null),
+ try([for n in var.service.network.networks : n.ip_address if n.ip_address != null][0], null)
+ )
+
+ # Generate environment variables for OAuth if it's enabled for the service.
+ oauth_envs = var.service.auth.oauth.enabled ? [
+ for env_name, output_key in var.service.auth.oauth.keys :
+ format(
+ "%s=%s",
+ env_name,
+ {
+ "client_id" = module.oauth_authentication[0].client_id,
+ "client_secret" = module.oauth_authentication[0].client_secret,
+ "well_known_url" = module.oauth_authentication[0].provider_info_url
+ }[output_key]
+ )
+ ] : []
+
+ # Create a map of Nginx Proxy Manager access lists by name for easy lookup.
+ npm_access_lists_by_name = { for al in data.nginxproxymanager_access_lists.access_lists.access_lists : al.name => al.id }
+}
diff --git a/terraform/modules/docker-service/main.tf b/terraform/modules/docker-service/main.tf
new file mode 100644
index 0000000..d94ac01
--- /dev/null
+++ b/terraform/modules/docker-service/main.tf
@@ -0,0 +1,21 @@
+
+
+module "service_container" {
+ source = "../../modules/docker"
+
+ icon = var.service.icon
+ web_ui = try(var.service.network.service_port, null) != null && local.service_ip_address != null ? "http://${local.service_ip_address}:${var.service.network.service_port}" : null
+ container_name = var.service.service_name
+ container_image = var.service.image_name
+ container_network_mode = var.service.network_mode
+ enable_gpu = var.service.enable_gpu
+ environment_vars = toset(concat(local.secret_envs, coalesce(local.oauth_envs, []), coalesce(var.service.env, [])))
+ mounts = var.service.mounts
+ container_capabilities = var.service.capabilities
+ commands = var.service.commands
+
+ # Attach the container to custom networks defined in the stack, but only if the service
+ # explicitly lists that network in its own configuration.
+ # The docker module expects a list of objects with `name` and `ipv4_address`.
+ networks = coalesce(var.service.network.networks, [])
+}
diff --git a/terraform/modules/docker-service/secrets.tf b/terraform/modules/docker-service/secrets.tf
new file mode 100644
index 0000000..50abd63
--- /dev/null
+++ b/terraform/modules/docker-service/secrets.tf
@@ -0,0 +1,23 @@
+data "infisical_projects" "home-net" {
+ # This assumes a constant project slug. Consider making this configurable
+ # via var.system if it needs to be more dynamic.
+ slug = var.system.infisical.project
+}
+
+data "infisical_secrets" "secrets" {
+ # Only fetch secrets if the stack configuration requests them.
+ count = (var.service.secrets != null && length(var.service.secrets) > 0) || (try(var.service.auth.proxy.enabled, false)) ? 1 : 0
+
+ env_slug = var.system.infisical.environment
+ workspace_id = data.infisical_projects.home-net.id
+ # This path corresponds to where the root `secrets` module stores secrets.
+ folder_path = var.system.infisical.folder
+}
+
+locals{
+ # Create a list of environment variables from the secrets map.
+ secret_envs = (var.service.secrets != null && length(var.service.secrets) > 0) ? [
+ for key, value in var.service.secrets :
+ format("%s=%s", key, data.infisical_secrets.secrets[0].secrets[value].value)
+ ] : []
+}
\ No newline at end of file
diff --git a/terraform/modules/docker-service/variables.tf b/terraform/modules/docker-service/variables.tf
new file mode 100644
index 0000000..c82a65d
--- /dev/null
+++ b/terraform/modules/docker-service/variables.tf
@@ -0,0 +1,71 @@
+variable "service" {
+ type = object({
+ # --- Core Service Definition ---
+ service_name = string
+ image_name = string
+ description = optional(string, "")
+ icon = optional(string, "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/docker.png")
+
+ # --- Execution & Runtime ---
+ commands = optional(list(string))
+ network_mode = optional(string)
+ enable_gpu = optional(bool, false)
+ capabilities = optional(object({
+ add = optional(list(string))
+ drop = optional(list(string))
+ }), {})
+
+ # --- Storage ---
+ mounts = optional(list(string))
+ volumes = optional(list(string))
+
+ # --- Environment & Secrets ---
+ env = optional(list(string))
+ secrets = optional(map(string))
+
+ # --- Networking & DNS ---
+ network = optional(object({
+ internal = optional(bool, false)
+ service_port = optional(number)
+ networks = optional(list(object({
+ name = string
+ ip_address = optional(string)
+ })), [])
+ }))
+ dns = object({
+ enabled = optional(bool, false),
+ internal = optional(bool, true),
+ domain_name = optional(string)
+ })
+
+ # --- Authentication ---
+ auth = optional(object({
+ enabled = optional(bool, false)
+ group = optional(string, "Uncategorized")
+ proxy = optional(object({
+ enabled = optional(bool, false)
+ user_secret = optional(string)
+ pass_secret = optional(string)
+ }), {})
+ oauth = optional(object({
+ enabled = optional(bool, false),
+ keys = optional(map(string), {}),
+ scopes = optional(list(string)),
+ redirect_uris = optional(list(string))
+ }), {})
+ }), {})
+ })
+
+ description = "Service and it's configuration"
+}
+
+variable "zone_name"{
+ type = string
+ default = ""
+ description = "DNS Zone name for service if DNS enabled."
+}
+
+variable "system" {
+ type = any
+ description = "A dynamic object containing system-wide configuration variables. Its attributes are accessed via lookup() for flexibility."
+}
\ No newline at end of file
diff --git a/terraform/modules/docker-service/versions.tf b/terraform/modules/docker-service/versions.tf
new file mode 100644
index 0000000..b08db4f
--- /dev/null
+++ b/terraform/modules/docker-service/versions.tf
@@ -0,0 +1,15 @@
+terraform {
+ required_providers {
+ random = {
+ source = "hashicorp/random"
+ }
+ nginxproxymanager = {
+ source = "Sander0542/nginxproxymanager"
+ version = "1.2.0"
+ }
+ infisical = {
+ source = "infisical/infisical"
+ version = "0.15.19"
+ }
+ }
+}
\ No newline at end of file
diff --git a/terraform/modules/docker-stack/auth.tf b/terraform/modules/docker-stack/auth.tf
deleted file mode 100644
index fc2ca36..0000000
--- a/terraform/modules/docker-stack/auth.tf
+++ /dev/null
@@ -1,64 +0,0 @@
-
-resource "random_password" "service_password" {
- for_each = {
- for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(v, "auth", {}), "proxy", false) == true
- }
-
- length = 24
- special = true
- override_special = "!#$%&*()-_=+[]{}<>:?"
-}
-
-
-module "proxy_authentication" {
- source = "../proxy_auth"
- for_each = {
- for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(v, "auth", {}), "proxy", false) == true
- }
-
- group = each.value.auth.group
- description = each.value.description
- # If a static IP is defined, use it. Otherwise, fall back to the service name,
- # which is resolvable within a Docker network.
- internal_host = "http://${coalesce(local.service_ip_addresses[each.key], each.value.service_name)}:${each.value.network.service_port}"
- external_host = each.value.dns.domain_name
- name = each.value.service_name
- username_attribute = "${each.value.service_name}_username"
- password_attribute = "${each.value.service_name}_password"
- create_access_group = true
- access_group_name = "tf_${each.value.service_name}"
- user_to_add_to_access_group = var.system.network_admin_username
- access_group_attributes = jsonencode(
- {
- "${each.value.service_name}_username" : "admin",
- "${each.value.service_name}_password" : random_password.service_password[each.key].result
- }
- )
-}
-
-module "oauth_authentication" {
- source = "../oauth_auth"
- for_each = {
- for k, v in var.stack.services : k => v if lookup(lookup(v, "auth", {}), "enabled", false) == true && lookup(lookup(lookup(v, "auth", {}), "oauth", {}), "enabled", false) == true
- }
-
- group = each.value.auth.group
- description = each.value.description
- name = each.value.service_name
- create_access_group = true
- access_group_name = "tf_${each.value.service_name}" //Update to allow for using custom groups rather than just generated
- user_to_add_to_access_group = var.system.network_admin_username //Update to allow for a list of users
- allowed_redirect_uris = concat(
- [
- {
- matching_mode = "strict",
- url = "https://${each.value.dns.domain_name}" }
- ],
- [
- for uri_path in coalesce(each.value.auth.oauth.redirect_uris, []) : {
- matching_mode = "strict",
- url = "https://${each.value.dns.domain_name}/${uri_path}"
- }
- ]
- )
-}
diff --git a/terraform/modules/docker-stack/dns.tf b/terraform/modules/docker-stack/dns.tf
deleted file mode 100644
index f8918a3..0000000
--- a/terraform/modules/docker-stack/dns.tf
+++ /dev/null
@@ -1,23 +0,0 @@
-data "nginxproxymanager_access_lists" "access_lists" {}
-
-module "service_dns" {
- source = "../dns"
- for_each = {
- for k, v in var.stack.services : k => v if lookup(v, "dns").enabled == true
- }
-
- internal_only = each.value.network.internal
- service_port = each.value.auth.proxy ? var.system.authentik.port : each.value.network.service_port
- zone_name = var.stack.zone_name
- domain_name = each.value.dns.domain_name
-
- # Really don't like having the ACLs hardcoded here...
- access_list_id = each.value.network.internal ? local.npm_access_lists_by_name["Internal Only"] : local.npm_access_lists_by_name["Cloudflare"]
- internal_host_ipv4 = var.system.proxy_ip
- # If not using proxy auth, point to the service's static IP. If no static IP,
- # fall back to the service name, which NPM can use as a hostname.
- service_ipv4 = each.value.auth.proxy ? var.system.authentik.ip_address : coalesce(local.service_ip_addresses[each.key], each.value.service_name)
- admin_email = var.system.network_admin_email
- dns_cloudflare_api_token = var.system.cloudflare_api_token
- external_host_ipv4 = var.system.public_facing_ip
-}
\ No newline at end of file
diff --git a/terraform/modules/docker-stack/locals.tf b/terraform/modules/docker-stack/locals.tf
index 836770a..f0bd6f6 100644
--- a/terraform/modules/docker-stack/locals.tf
+++ b/terraform/modules/docker-stack/locals.tf
@@ -1,66 +1,25 @@
locals {
+ # Create a map of service IP addresses, trying different network names.
service_ip_addresses = {
- for service_key, service_config in var.stack.services : service_key => try(
- [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br1" && n.ip_address != null][0],
- [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br0" && n.ip_address != null][0],
- [for n in try(service_config.network.networks, []) : n.ip_address if n.ip_address != null][0],
- null
- )
+ # This logic finds the first available IP address for a service based on a preferred network order (br1, then br0, then any).
+ # It safely handles cases where a service has no network configuration, no networks defined, or no static IP on any network.
+ # It concatenates lists of IPs from each check and adds a final `[null]` to ensure the list is never empty, then takes the first element.
+ for service_key, service_config in var.stack.services : service_key => concat(
+ [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br1" && n.ip_address != null],
+ [for n in try(service_config.network.networks, []) : n.ip_address if n.name == "br0" && n.ip_address != null],
+ [for n in try(service_config.network.networks, []) : n.ip_address if n.ip_address != null],
+ [null]
+ )[0]
}
- # Networks that this module should actually create.
- # Filter out pre-existing host networks.
+ # Filter out networks that already exist.
creatable_networks = {
for name, config in coalesce(var.stack.networks, {}) : name => config
if !contains(var.system.existing_networks, name)
}
- # A map of secrets fetched from Infisical.
- # This will fail the plan if a secret listed in `var.stack.generated_secrets` is not found
- # in Infisical, which is the desired behavior to prevent deploying with missing credentials.
- generated_secrets = {
- for secret_name in toset(coalesce(var.stack.generated_secrets, [])) :
- secret_name => data.infisical_secrets.generated_secrets[0].secrets[secret_name].value
- }
-
- # For each service, process its environment variables to substitute secret placeholders.
- processed_envs = {
- for service_key, service_config in var.stack.services : service_key => [
- for env_string in coalesce(service_config.env, []) : (
- # This pattern ensures that for each env var, we get either the string with the secret
- # replaced, or the original string if no placeholder was found.
- # The one() function enforces a "one secret per line" rule, failing if multiple placeholders exist.
- one(concat(
- [
- for secret_name, secret_value in local.generated_secrets :
- replace(env_string, format("$${%s}", secret_name), secret_value)
- if strcontains(env_string, format("$${%s}", secret_name))
- ],
- # This list provides the fallback original string if no secret placeholder was found.
- !anytrue([for name in keys(local.generated_secrets) : strcontains(env_string, format("$${%s}", name))]) ? [env_string] : []
- ))
- )
- ]
- }
-
- oauth_envs = {
- for service_key, service_config in var.stack.services : service_key => (
- lookup(lookup(service_config, "auth", {}), "oauth", { enabled = false }).enabled ? [
- for env_name, output_key in lookup(lookup(service_config, "auth", {}), "oauth", { keys = {} }).keys :
- format(
- "%s=%s",
- env_name,
- # This map translates the key from your YAML (e.g., "client_id")
- # to the corresponding attribute of the created Authentik resource.
- {
- "client_id" : module.oauth_authentication[service_key].client_id,
- "client_secret" : module.oauth_authentication[service_key].client_secret,
- "well_known_url" : module.oauth_authentication[service_key].provider_info_url
- }[output_key] # Use the value from the YAML keys map to look up the correct output
- )
- ] : []
- )
- }
-
+ # Create a map of Nginx Proxy Manager access lists by name.
npm_access_lists_by_name = { for al in data.nginxproxymanager_access_lists.access_lists.access_lists : al.name => al.id }
-}
\ No newline at end of file
+}
+
+data "nginxproxymanager_access_lists" "access_lists" {}
diff --git a/terraform/modules/docker-stack/main.tf b/terraform/modules/docker-stack/main.tf
index e36bf55..98ccb33 100644
--- a/terraform/modules/docker-stack/main.tf
+++ b/terraform/modules/docker-stack/main.tf
@@ -4,16 +4,6 @@ data "infisical_projects" "home-net" {
slug = "home-net-ln-sy"
}
-data "infisical_secrets" "generated_secrets" {
- # Only fetch secrets if the stack configuration requests them.
- count = length(coalesce(var.stack.generated_secrets, [])) > 0 ? 1 : 0
-
- env_slug = "dev"
- workspace_id = data.infisical_projects.home-net.id
- # This path corresponds to where the root `secrets` module stores secrets.
- folder_path = "/generated/credentials"
-}
-
module "custom_network" {
count = length(local.creatable_networks) > 0 ? 1 : 0
source = "../../modules/docker-network"
@@ -22,7 +12,15 @@ module "custom_network" {
module "service_container" {
for_each = var.stack.services
- source = "../../modules/docker"
+ source = "../../modules/docker-service"
+
+ zone_name = var.stack.zone_name
+ service = each.value
+ system = var.system
+
+/*
+Still need to combine environment, mounts, and networks!
+
icon = each.value.icon
web_ui = try(each.value.network.service_port, null) != null && local.service_ip_addresses[each.key] != null ? "http://${local.service_ip_addresses[each.key]}:${each.value.network.service_port}" : null
@@ -39,4 +37,5 @@ module "service_container" {
# explicitly lists that network in its own configuration.
# The docker module expects a list of objects with `name` and `ipv4_address`.
networks = coalesce(each.value.network.networks, [])
+ */
}
diff --git a/terraform/modules/docker-stack/outputs.tf b/terraform/modules/docker-stack/outputs.tf
index 659b1d9..e69de29 100644
--- a/terraform/modules/docker-stack/outputs.tf
+++ b/terraform/modules/docker-stack/outputs.tf
@@ -1,5 +0,0 @@
-output "generated_secrets" {
- description = "A map of the dynamically generated secrets for the stack."
- value = local.generated_secrets
- sensitive = true
-}
\ No newline at end of file
diff --git a/terraform/modules/docker-stack/variables.tf b/terraform/modules/docker-stack/variables.tf
index 1bbd670..91ea38c 100644
--- a/terraform/modules/docker-stack/variables.tf
+++ b/terraform/modules/docker-stack/variables.tf
@@ -3,7 +3,6 @@ variable "stack" {
env = optional(list(string))
mounts = optional(list(string))
volumes = optional(list(string))
- generated_secrets = optional(list(string))
zone_name = optional(string, null)
networks = optional(map(object({
internal = optional(bool, false)
@@ -20,7 +19,8 @@ variable "stack" {
mounts = optional(list(string))
enable_gpu = optional(bool, false)
env = optional(list(string))
- volumes = optional(list(string))
+ volumes = optional(list(string)),
+ secrets = optional(map(string)),
capabilities = optional(object({
add = optional(list(string))
drop = optional(list(string))
@@ -30,17 +30,21 @@ variable "stack" {
internal = optional(bool, true),
domain_name = optional(string)
})
- auth = optional(object({
- enabled = optional(bool, false),
- proxy = optional(bool, false),
- group = optional(string, "Uncategorized"),
- oauth = optional(object({
- enabled = optional(bool, false),
- keys = optional(map(string), {}),
- scopes = optional(list(string)),
- redirect_uris = optional(list(string))
- }), {})
- }))
+ auth = optional(object({
+ enabled = optional(bool, false)
+ group = optional(string, "Uncategorized")
+ proxy = optional(object({
+ enabled = optional(bool, false)
+ user_secret = optional(string)
+ pass_secret = optional(string)
+ }), {})
+ oauth = optional(object({
+ enabled = optional(bool, false),
+ keys = optional(map(string), {}),
+ scopes = optional(list(string)),
+ redirect_uris = optional(list(string))
+ }), {})
+ }), {})
network = optional(object({
internal = optional(bool, false)
service_port = optional(number)
diff --git a/terraform/modules/generated_secrets/README.md b/terraform/modules/generated_secrets/README.md
deleted file mode 100644
index 7916d8f..0000000
--- a/terraform/modules/generated_secrets/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-
-## Requirements
-
-| Name | Version |
-|------|---------|
-| [infisical](#requirement\_infisical) | 0.15.19 |
-
-## Providers
-
-| Name | Version |
-|------|---------|
-| [infisical](#provider\_infisical) | 0.15.19 |
-| [random](#provider\_random) | n/a |
-
-## Resources
-
-| Name | Type |
-|------|------|
-| [infisical_secret.stored_secret](https://registry.terraform.io/providers/infisical/infisical/0.15.19/docs/resources/secret) | resource |
-| [random_password.generated_secret](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
-| [infisical_projects.home-net](https://registry.terraform.io/providers/infisical/infisical/0.15.19/docs/data-sources/projects) | data source |
-
-## Inputs
-
-| Name | Description | Type | Default | Required |
-|------|-------------|------|---------|:--------:|
-| [name](#input\_name) | List of secret names to generate | `list(string)` | n/a | yes |
-| [project\_slug](#input\_project\_slug) | Slug of project to store secret in | `string` | `"home-net-ln-sy"` | no |
-
-## Outputs
-
-| Name | Description |
-|------|-------------|
-| [generated\_secrets\_list](#output\_generated\_secrets\_list) | A list of objects, each containing the name and generated value of a secret. |
-| [generated\_secrets\_map](#output\_generated\_secrets\_map) | A map of generated secret names to their values. |
-
\ No newline at end of file
diff --git a/terraform/modules/generated_secrets/locals.tf b/terraform/modules/generated_secrets/locals.tf
deleted file mode 100644
index fd06c19..0000000
--- a/terraform/modules/generated_secrets/locals.tf
+++ /dev/null
@@ -1,17 +0,0 @@
-locals {
- # Create a map of secret names to their generated password values.
- # This is the ideal format for a for_each loop, ensuring each resource
- # instance has a unique, string-based key.
- generated_secrets_map = {
- for name, password_obj in random_password.generated_secret : name => password_obj.result
- }
-
- # Create a list of objects from the map.
- # This is useful for outputs that require a list format.
- generated_secret_list = [
- for name, value in local.generated_secrets_map : {
- name = name
- value = value
- }
- ]
-}
\ No newline at end of file
diff --git a/terraform/modules/generated_secrets/main.tf b/terraform/modules/generated_secrets/main.tf
deleted file mode 100644
index d411dd6..0000000
--- a/terraform/modules/generated_secrets/main.tf
+++ /dev/null
@@ -1,30 +0,0 @@
-terraform {
- required_providers {
- infisical = {
- source = "infisical/infisical"
- version = "0.15.19"
- }
- }
-}
-
-data "infisical_projects" "home-net" {
- slug = var.project_slug
-}
-
-resource "random_password" "generated_secret" {
- for_each = toset(var.name)
-
- length = 24
- special = true
- override_special = "!#$%&*()-_=+[]{}<>:?"
-}
-
-resource "infisical_secret" "stored_secret" {
- for_each = local.generated_secrets_map
-
- name = each.key
- value = each.value
- env_slug = "dev"
- workspace_id = data.infisical_projects.home-net.id
- folder_path = "/generated/credentials"
-}
\ No newline at end of file
diff --git a/terraform/modules/generated_secrets/outputs.tf b/terraform/modules/generated_secrets/outputs.tf
deleted file mode 100644
index 51f325b..0000000
--- a/terraform/modules/generated_secrets/outputs.tf
+++ /dev/null
@@ -1,11 +0,0 @@
-output "generated_secrets_list" {
- description = "A list of objects, each containing the name and generated value of a secret."
- value = local.generated_secret_list
- sensitive = true
-}
-
-output "generated_secrets_map" {
- description = "A map of generated secret names to their values."
- value = local.generated_secrets_map
- sensitive = true
-}
\ No newline at end of file
diff --git a/terraform/modules/generated_secrets/variables.tf b/terraform/modules/generated_secrets/variables.tf
deleted file mode 100644
index ff41a85..0000000
--- a/terraform/modules/generated_secrets/variables.tf
+++ /dev/null
@@ -1,11 +0,0 @@
-variable "name" {
- type = list(string)
- description = "List of secret names to generate"
-}
-
-variable "project_slug" {
- type = string
- description = "Slug of project to store secret in"
- default = "home-net-ln-sy"
-
-}
\ No newline at end of file
diff --git a/terraform/modules/oauth_auth/service.tf b/terraform/modules/oauth_auth/service.tf
index c646008..57aa214 100644
--- a/terraform/modules/oauth_auth/service.tf
+++ b/terraform/modules/oauth_auth/service.tf
@@ -1,4 +1,4 @@
-resource "random_string" "example" {
+resource "random_string" "client_id" {
length = 16
special = false
upper = true
@@ -43,7 +43,7 @@ data "authentik_certificate_key_pair" "generated" {
resource "authentik_provider_oauth2" "name" {
name = lower(replace(var.name, " ", "-"))
- client_id = random_string.example.result
+ client_id = random_string.client_id.result
authorization_flow = data.authentik_flow.default-authorization-flow.id
invalidation_flow = data.authentik_flow.default-invalidation-flow.id
signing_key = data.authentik_certificate_key_pair.generated.id
diff --git a/terraform/providers.tf b/terraform/providers.tf
index 901c4ea..3652c4e 100644
--- a/terraform/providers.tf
+++ b/terraform/providers.tf
@@ -4,16 +4,16 @@ provider "nginxproxymanager" {
password = var.nginx_proxy_pass
}
-
provider "docker" {
host = "unix:///var/run/docker.sock"
+// host = "ssh://root@192.168.1.41:22"
+// ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
}
/*
provider "docker" {
- host = "ssh://root@192.168.1.41:22"
- ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
-}*/
+}
+*/
provider "technitium" {
url = var.technitium_host
diff --git a/terraform/services.tf b/terraform/services.tf
index e0fc563..b58682e 100644
--- a/terraform/services.tf
+++ b/terraform/services.tf
@@ -1,31 +1,15 @@
-module "secrets" {
- source = "./modules/generated_secrets"
-
- name = local.secrets
-}
-
module "stacks" {
for_each = local.stacks
source = "./modules/docker-stack"
stack = each.value
system = local.system
-
- //depends_on = [ module.secrets ]
}
-module "flaresolverr_service" {
- source = "./modules/docker"
-
- container_name = local.services.flaresolverr.service_name
- container_image = local.services.flaresolverr.image_name
- networks = local.services.flaresolverr.network.networks
- environment_vars = local.services.flaresolverr.env
-}
+module "services"{
+ for_each = local.services
+ source = "./modules/docker-service"
-module "delugevpn_service" {
- source = "./services/deluge-vpn"
- service = local.services.deluge-vpn
- vpn_pass = var.vpn_pass
- vpn_user = var.vpn_user
+ service = each.value
+ system = local.system
}
\ No newline at end of file