Archived
Initial infrastructure mono-repo scaffold
Consolidates nixos, docker, raspi, and debian-configuration into a single infrastructure-as-code repo. Includes: - ansible/: full inventory + proxmox-hardening, freeipa, and raspberrypi roles (converted from debian-configuration bash scripts) - terraform/: Proxmox VMs, Dynu DNS, Pi-hole (decommissioned stub), Docker container catalog — migrated from docker/infrastructure/terraform/ - stacks/docker/, stacks/raspi/, nixos/: placeholder READMEs pending git subtree population (see implementation plan) - docs/: internal MkDocs site with architecture, network topology, runbooks, and drift-detection guide; external sanitized site - scripts/: drift-detect.sh, docs-build.sh, install-hooks.sh, check-secrets.sh - CI: secret-scan (push/PR), drift-detect (daily), docs-build (on change) - Pi-hole removed throughout — DNS is FreeIPA, DHCP is router See docs/internal/implementation-plan.md for the phased rollout after pushing to Gitea. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvNjoxTWEDkhXsd1Dq2ETP
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
crash.log
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
*.tfplan
|
||||
plan.out
|
||||
state/
|
||||
artifacts/
|
||||
@@ -0,0 +1,52 @@
|
||||
# terraform/
|
||||
|
||||
Infrastructure state management. Each subdirectory is an independent Terraform workspace
|
||||
with its own state backend.
|
||||
|
||||
## Workspaces
|
||||
|
||||
| Directory | What it manages | State backend |
|
||||
|-----------|----------------|---------------|
|
||||
| `proxmox/` | Proxmox VMs and LXCs on pve1 | Remote (configure in bootstrap/) |
|
||||
| `dns/` | Dynu dynamic DNS records | Remote |
|
||||
| `docker/` | Docker container catalog (documentation-only, read-only) | Local |
|
||||
| `bootstrap/` | Remote state backend resources | Local (chicken-and-egg) |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Dry-run: show what would change
|
||||
cd terraform/proxmox
|
||||
terraform init
|
||||
terraform plan
|
||||
|
||||
# Detect drift only (exit 2 = drift, exit 0 = in sync)
|
||||
terraform plan -detailed-exitcode
|
||||
|
||||
# Apply (confirm first — always review the plan)
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Credentials
|
||||
|
||||
Never commit credentials. Use `terraform.tfvars` (git-ignored) or environment variables:
|
||||
|
||||
```bash
|
||||
# Proxmox
|
||||
export TF_VAR_proxmox_endpoint="https://pve1.sweet.home:8006/"
|
||||
export TF_VAR_proxmox_api_token_id="terraform@pve!tf"
|
||||
export TF_VAR_proxmox_api_token_secret="<token>"
|
||||
|
||||
# Dynu DNS
|
||||
export TF_VAR_dynu_api_key="<api_key>"
|
||||
```
|
||||
|
||||
## Importing existing resources
|
||||
|
||||
See `docs/internal/implementation-plan.md` Phase 2 for the `terraform import` commands
|
||||
to bring existing Proxmox resources under state management.
|
||||
|
||||
## Drift detection
|
||||
|
||||
The `drift-detect` CI workflow runs `terraform plan -detailed-exitcode` daily on all
|
||||
workspaces and sends a Gotify notification if any drift is detected.
|
||||
@@ -0,0 +1,187 @@
|
||||
# Dynu Terraform Layer (Brownfield DNS Reconciliation)
|
||||
|
||||
This Terraform root is for **Dynu DNS brownfield reconciliation**. The intended pattern is:
|
||||
|
||||
1. Import the existing root domain object.
|
||||
2. Read inventory through `data.dynu_dns_records.root`.
|
||||
3. Generate reviewable `dynu_dns_record` resources and import commands.
|
||||
4. Import every existing DNS record into matching Terraform resources.
|
||||
5. Use `terraform plan` as the reconciliation check before any apply.
|
||||
|
||||
## Provider behavior to keep in mind
|
||||
|
||||
- Source: `beatz174-bit/dynu`
|
||||
- `dynu_domain` import requires a **numeric Dynu domain ID**.
|
||||
- Importing `dynu_domain` imports only the root domain object.
|
||||
- It **does not** import DNS records/subdomains.
|
||||
- `dynu_dns_record` imports require `<domain_id>/<record_id>`.
|
||||
|
||||
## Variables
|
||||
|
||||
- `dynu_root_domain` (default: `lan.ddnsgeek.com`)
|
||||
- `dynu_api_key` (sensitive)
|
||||
- `dynu_username` / `dynu_password` (optional)
|
||||
|
||||
## Safe validation commands
|
||||
|
||||
```bash
|
||||
cd infrastructure/terraform/dynu
|
||||
terraform fmt -check -recursive
|
||||
terraform init -backend=false -input=false
|
||||
terraform validate
|
||||
python3 -m py_compile scripts/generate-brownfield-records.py
|
||||
```
|
||||
|
||||
## Brownfield workflow
|
||||
|
||||
```bash
|
||||
cd infrastructure/terraform/dynu
|
||||
|
||||
terraform init
|
||||
terraform import dynu_domain.lan_ddnsgeek_com '<numeric-dynu-domain-id>'
|
||||
|
||||
terraform apply -refresh-only
|
||||
terraform output -json dynu_dns_records > /tmp/dynu-records.json
|
||||
|
||||
python3 scripts/generate-brownfield-records.py --dry-run
|
||||
python3 scripts/generate-brownfield-records.py --overwrite
|
||||
|
||||
# Review generated/dynu_dns_records.generated.tf
|
||||
# Review generated/import-dynu-dns-records.sh
|
||||
|
||||
bash generated/import-dynu-dns-records.sh
|
||||
|
||||
terraform plan
|
||||
```
|
||||
|
||||
## What each component means
|
||||
|
||||
- `data.dynu_dns_records.root`: read-only live inventory from Dynu.
|
||||
- `generated/dynu_dns_records.generated.tf`: generated management-intent resources; includes `prevent_destroy = true` on each record.
|
||||
- `generated/import-dynu-dns-records.sh`: imports each discovered record to its generated `dynu_dns_record` address using `<domain_id>/<record_id>`.
|
||||
- `terraform plan` after imports: reconciliation checkpoint. Any create/update/delete must be reviewed manually before apply.
|
||||
|
||||
## Generated artifacts
|
||||
|
||||
The helper script writes these files under `generated/`:
|
||||
|
||||
- `generated/dynu_dns_records_inventory.json`
|
||||
- `generated/dynu_dns_records.generated.tf`
|
||||
- `generated/import-dynu-dns-records.sh`
|
||||
|
||||
These are generated outputs meant for operator review before use in production.
|
||||
|
||||
|
||||
### Generator output selection (interactive + automation)
|
||||
|
||||
The brownfield generator defaults to Terraform output `dynu_dns_records`:
|
||||
|
||||
```bash
|
||||
python3 scripts/generate-brownfield-records.py --dry-run
|
||||
```
|
||||
|
||||
If the default output is missing/unusable and stdin is interactive, the script shows a picker of available Terraform outputs and indicates which ones are usable for DNS imports.
|
||||
|
||||
```bash
|
||||
# Interactive mode: choose from available Terraform outputs
|
||||
python3 scripts/generate-brownfield-records.py --dry-run
|
||||
|
||||
# Non-interactive mode: specify output explicitly
|
||||
python3 scripts/generate-brownfield-records.py \
|
||||
--records-output dynu_dns_inventory \
|
||||
--dry-run
|
||||
|
||||
# Disable menu and fail fast
|
||||
python3 scripts/generate-brownfield-records.py \
|
||||
--no-interactive \
|
||||
--dry-run
|
||||
|
||||
# Use saved terraform output JSON and choose interactively
|
||||
terraform output -json > generated/terraform-output.json
|
||||
python3 scripts/generate-brownfield-records.py \
|
||||
--from-file generated/terraform-output.json \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The menu shows Terraform outputs currently stored in state.
|
||||
- If newly added outputs do not appear, run:
|
||||
|
||||
```bash
|
||||
terraform apply -refresh-only
|
||||
```
|
||||
|
||||
- The selected output must contain real Dynu provider record fields:
|
||||
- `id`
|
||||
- `domain_id`
|
||||
- `hostname`
|
||||
- `record_type`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plan shows a large wall of `+` values under outputs
|
||||
|
||||
Cause:
|
||||
|
||||
Terraform is planning to save **new output values** to state (for example, live records from `data.dynu_dns_records.root`). This is not creating DNS records by itself.
|
||||
|
||||
How to verify:
|
||||
|
||||
- Output-only changes appear under `Changes to Outputs`.
|
||||
- Real DNS changes appear as `dynu_dns_record` resource create/update/delete actions.
|
||||
|
||||
Use:
|
||||
|
||||
```bash
|
||||
terraform apply -refresh-only
|
||||
```
|
||||
|
||||
to persist refreshed data source and output values only.
|
||||
|
||||
### Error: `There is no function named "regexreplace"`
|
||||
|
||||
Cause:
|
||||
|
||||
`regexreplace` is not a Terraform function. Resource-name slugification should not be implemented in Terraform HCL for this workflow.
|
||||
|
||||
Fix:
|
||||
|
||||
- Keep `inventory.tf` focused on reading live records via `data.dynu_dns_records.root`.
|
||||
- Keep Terraform outputs simple (for example, `<domain_id>/<record_id>` mappings).
|
||||
- Let `scripts/generate-brownfield-records.py` generate Terraform-safe resource names with Python `tf_name(record)`.
|
||||
|
||||
### Error: `'"'"'dynu_dns_records'"'"'`
|
||||
|
||||
Cause:
|
||||
|
||||
The helper script reads `terraform output -json` and expects an output named `dynu_dns_records`.
|
||||
|
||||
Fix:
|
||||
|
||||
```bash
|
||||
cd infrastructure/terraform/dynu
|
||||
terraform init
|
||||
terraform apply -refresh-only
|
||||
terraform output -json | jq 'keys'
|
||||
```
|
||||
|
||||
Confirm `dynu_dns_records` appears in the key list.
|
||||
|
||||
If it does not, check that the Terraform config contains:
|
||||
|
||||
```hcl
|
||||
data "dynu_dns_records" "root" {
|
||||
hostname = var.dynu_root_domain
|
||||
}
|
||||
|
||||
output "dynu_dns_records" {
|
||||
value = data.dynu_dns_records.root.records
|
||||
}
|
||||
```
|
||||
|
||||
Then rerun:
|
||||
|
||||
```bash
|
||||
python3 scripts/generate-brownfield-records.py --dry-run
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
data "dynu_domain" "lan" {
|
||||
hostname = "lan.ddnsgeek.com"
|
||||
}
|
||||
|
||||
output "dynu_domain_id" {
|
||||
value = data.dynu_domain.lan.domain.id
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
locals {
|
||||
dynu_domain = var.dynu_root_domain
|
||||
}
|
||||
|
||||
# Import-first resource skeleton for the production Dynu zone.
|
||||
# `name` is required by provider schema and can be reconciled after import.
|
||||
resource "dynu_domain" "lan_ddnsgeek_com" {
|
||||
name = local.dynu_domain
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# GENERATED FILE - REVIEW BEFORE USE
|
||||
#
|
||||
# Generated from Dynu brownfield DNS inventory.
|
||||
# Do not blindly apply this file to production DNS.
|
||||
# Import records into Terraform state before allowing Terraform to manage them.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
|
||||
hostname = "auth.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "auth"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
|
||||
hostname = "auth.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "auth"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
|
||||
hostname = "edge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "edge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
|
||||
hostname = "edge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "edge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
|
||||
hostname = "familytree.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "familytree"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
|
||||
hostname = "familytree.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "familytree"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
|
||||
hostname = "gitea.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "gitea"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
|
||||
hostname = "gitea.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "gitea"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
|
||||
hostname = "gotify.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "gotify"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
|
||||
hostname = "gotify.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "gotify"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
|
||||
hostname = "grafana.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "grafana"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
|
||||
hostname = "grafana.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "grafana"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
|
||||
hostname = "influxdb.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "influxdb"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
|
||||
hostname = "influxdb.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "influxdb"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
|
||||
hostname = "kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 90
|
||||
enabled = true
|
||||
content = "120.155.99.146"
|
||||
node_name = "kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
|
||||
hostname = "lan.ddnsgeek.com"
|
||||
record_type = "SOA"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
|
||||
hostname = "monitor-kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "monitor-kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
|
||||
hostname = "monitor-kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "monitor-kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
|
||||
hostname = "mtls-bridge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "mtls-bridge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
|
||||
hostname = "mtls-bridge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "mtls-bridge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
|
||||
hostname = "nextcloud.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "nextcloud"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
|
||||
hostname = "nextcloud.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "nextcloud"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
|
||||
hostname = "node-red.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "node-red"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
|
||||
hostname = "node-red.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "node-red"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
|
||||
hostname = "passbolt.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "passbolt"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
|
||||
hostname = "passbolt.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "passbolt"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
|
||||
hostname = "portainer.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "portainer"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
|
||||
hostname = "portainer.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "portainer"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
|
||||
hostname = "prometheus.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "prometheus"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
|
||||
hostname = "prometheus.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "prometheus"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
|
||||
hostname = "searxng.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "searxng"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
|
||||
hostname = "searxng.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "searxng"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
|
||||
hostname = "shifts.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "shifts"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
|
||||
hostname = "shifts.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "shifts"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
|
||||
hostname = "stockfill.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "stockfill"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
|
||||
hostname = "stockfill.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "stockfill"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
|
||||
hostname = "traefik.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "traefik"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
|
||||
hostname = "traefik.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "traefik"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# GENERATED FILE - REVIEW BEFORE USE
|
||||
#
|
||||
# Generated from Dynu brownfield DNS inventory.
|
||||
# Do not blindly apply this file to production DNS.
|
||||
# Import records into Terraform state before allowing Terraform to manage them.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_18483099" {
|
||||
hostname = "auth.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "auth"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "auth_lan_ddnsgeek_com_a_19646048" {
|
||||
hostname = "auth.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "auth"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_10453241" {
|
||||
hostname = "edge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "edge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "edge_lan_ddnsgeek_com_a_19646062" {
|
||||
hostname = "edge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "edge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_17017685" {
|
||||
hostname = "familytree.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "familytree"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "familytree_lan_ddnsgeek_com_a_19646056" {
|
||||
hostname = "familytree.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "familytree"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_14682463" {
|
||||
hostname = "gitea.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "gitea"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gitea_lan_ddnsgeek_com_a_19646063" {
|
||||
hostname = "gitea.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "gitea"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_17439061" {
|
||||
hostname = "gotify.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "gotify"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "gotify_lan_ddnsgeek_com_a_19646047" {
|
||||
hostname = "gotify.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "gotify"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_18113762" {
|
||||
hostname = "grafana.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "grafana"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "grafana_lan_ddnsgeek_com_a_19646050" {
|
||||
hostname = "grafana.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "grafana"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_18562198" {
|
||||
hostname = "influxdb.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "influxdb"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "influxdb_lan_ddnsgeek_com_a_19646059" {
|
||||
hostname = "influxdb.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "influxdb"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "kuma_lan_ddnsgeek_com_a_17454978" {
|
||||
hostname = "kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 60
|
||||
enabled = true
|
||||
content = "120.155.99.146"
|
||||
node_name = "kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "lan_ddnsgeek_com_soa_8299670" {
|
||||
hostname = "lan.ddnsgeek.com"
|
||||
record_type = "SOA"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "ns1.dynu.com. administrator.dynu.com. 0 3600 900 604800 300"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_17462342" {
|
||||
hostname = "monitor-kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "monitor-kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "monitor_kuma_lan_ddnsgeek_com_a_19646051" {
|
||||
hostname = "monitor-kuma.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "monitor-kuma"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19232643" {
|
||||
hostname = "mtls-bridge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "mtls-bridge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "mtls_bridge_lan_ddnsgeek_com_a_19646058" {
|
||||
hostname = "mtls-bridge.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "mtls-bridge"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_10453260" {
|
||||
hostname = "nextcloud.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "nextcloud"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "nextcloud_lan_ddnsgeek_com_a_19646057" {
|
||||
hostname = "nextcloud.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "nextcloud"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19041230" {
|
||||
hostname = "node-red.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "node-red"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "node_red_lan_ddnsgeek_com_a_19646053" {
|
||||
hostname = "node-red.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "node-red"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_10453262" {
|
||||
hostname = "passbolt.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "passbolt"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "passbolt_lan_ddnsgeek_com_a_19646049" {
|
||||
hostname = "passbolt.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "passbolt"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_17458810" {
|
||||
hostname = "portainer.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "portainer"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "portainer_lan_ddnsgeek_com_a_19646046" {
|
||||
hostname = "portainer.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "portainer"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_18483311" {
|
||||
hostname = "prometheus.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "prometheus"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "prometheus_lan_ddnsgeek_com_a_19646061" {
|
||||
hostname = "prometheus.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "prometheus"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_10453263" {
|
||||
hostname = "searxng.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "searxng"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "searxng_lan_ddnsgeek_com_a_19646055" {
|
||||
hostname = "searxng.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "searxng"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_15901565" {
|
||||
hostname = "shifts.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "shifts"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "shifts_lan_ddnsgeek_com_a_19646052" {
|
||||
hostname = "shifts.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "shifts"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_17081867" {
|
||||
hostname = "stockfill.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "stockfill"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "stockfill_lan_ddnsgeek_com_a_19646060" {
|
||||
hostname = "stockfill.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "stockfill"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_10453240" {
|
||||
hostname = "traefik.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
dynamic = true
|
||||
node_name = "traefik"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "dynu_dns_record" "traefik_lan_ddnsgeek_com_a_19646054" {
|
||||
hostname = "traefik.lan.ddnsgeek.com"
|
||||
record_type = "A"
|
||||
ttl = 120
|
||||
enabled = true
|
||||
content = "167.179.167.166"
|
||||
group = "home"
|
||||
node_name = "traefik"
|
||||
|
||||
lifecycle {
|
||||
prevent_destroy = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"domain":"lan.ddnsgeek.com","provider":"dynu","record_count":15,"records":{"auth":{"fqdn":"auth.lan.ddnsgeek.com","hostname":"auth","proxied":null,"purpose":"Authentication portal","record_type":null,"service":"authelia","source":"core/authelia/docker-compose.yml","target":null,"ttl":null},"familytree":{"fqdn":"familytree.lan.ddnsgeek.com","hostname":"familytree","proxied":null,"purpose":"Family tree application","record_type":null,"service":"gramps","source":"apps/gramps/docker-compose.yml","target":null,"ttl":null},"gitea":{"fqdn":"gitea.lan.ddnsgeek.com","hostname":"gitea","proxied":null,"purpose":"Gitea service endpoint","record_type":null,"service":"gitea","source":"apps/gitea/docker-compose.yml","target":null,"ttl":null},"gotify":{"fqdn":"gotify.lan.ddnsgeek.com","hostname":"gotify","proxied":null,"purpose":"Gotify notifications","record_type":null,"service":"gotify","source":"monitoring/gotify/docker-compose.yml","target":null,"ttl":null},"grafana":{"fqdn":"grafana.lan.ddnsgeek.com","hostname":"grafana","proxied":null,"purpose":"Grafana monitoring UI","record_type":null,"service":"grafana","source":"monitoring/grafana/docker-compose.yml","target":null,"ttl":null},"influxdb":{"fqdn":"influxdb.lan.ddnsgeek.com","hostname":"influxdb","proxied":null,"purpose":"InfluxDB metrics endpoint","record_type":null,"service":"influxdb","source":"monitoring/influxdb/docker-compose.yml","target":null,"ttl":null},"monitor_kuma":{"fqdn":"monitor-kuma.lan.ddnsgeek.com","hostname":"monitor-kuma","proxied":null,"purpose":"Uptime Kuma monitoring UI","record_type":null,"service":"uptime-kuma","source":"monitoring/uptime-kuma/docker-compose.yml","target":null,"ttl":null},"mtls_bridge":{"fqdn":"mtls-bridge.lan.ddnsgeek.com","hostname":"mtls-bridge","proxied":null,"purpose":"mTLS bridge API","record_type":null,"service":"mtls-bridge","source":"monitoring/mtls-bridge/docker-compose.yml","target":null,"ttl":null},"nextcloud":{"fqdn":"nextcloud.lan.ddnsgeek.com","hostname":"nextcloud","proxied":null,"purpose":"Nextcloud service endpoint","record_type":null,"service":"nextcloud-webapp","source":"apps/nextcloud/docker-compose.yml","target":null,"ttl":null},"node_red":{"fqdn":"node-red.lan.ddnsgeek.com","hostname":"node-red","proxied":null,"purpose":"Node-RED automation UI/API","record_type":null,"service":"node-red","source":"monitoring/node-red/docker-compose.yml","target":null,"ttl":null},"passbolt":{"fqdn":"passbolt.lan.ddnsgeek.com","hostname":"passbolt","proxied":null,"purpose":"Passbolt password management","record_type":null,"service":"passbolt-webapp","source":"apps/passbolt/docker-compose.yml","target":null,"ttl":null},"portainer":{"fqdn":"portainer.lan.ddnsgeek.com","hostname":"portainer","proxied":null,"purpose":"Portainer admin endpoint","record_type":null,"service":"portainer","source":"monitoring/portainer/docker-compose.yml","target":null,"ttl":null},"prometheus":{"fqdn":"prometheus.lan.ddnsgeek.com","hostname":"prometheus","proxied":null,"purpose":"Prometheus metrics endpoint","record_type":null,"service":"prometheus","source":"monitoring/prometheus/docker-compose.yml","target":null,"ttl":null},"searxng":{"fqdn":"searxng.lan.ddnsgeek.com","hostname":"searxng","proxied":null,"purpose":"SearXNG search endpoint","record_type":null,"service":"searxng","source":"apps/searxng/docker-compose.yml","target":null,"ttl":null},"traefik":{"fqdn":"traefik.lan.ddnsgeek.com","hostname":"traefik","proxied":null,"purpose":"Traefik dashboard/API endpoint","record_type":null,"service":"traefik","source":"core/traefik/docker-compose.yml","target":null,"ttl":null}}}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# GENERATED FILE - REVIEW BEFORE USE
|
||||
#
|
||||
# Imports existing Dynu DNS records into Terraform state.
|
||||
# Does not apply changes.
|
||||
# ---------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${TF_ROOT}"
|
||||
|
||||
# Re-running imports will fail for resources already in state.
|
||||
# This script skips imports when state already contains the resource address.
|
||||
|
||||
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099'
|
||||
else
|
||||
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_18483099' '9695470/18483099'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048'
|
||||
else
|
||||
terraform import 'dynu_dns_record.auth_lan_ddnsgeek_com_a_19646048' '9695470/19646048'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241'
|
||||
else
|
||||
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_10453241' '9695470/10453241'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062'
|
||||
else
|
||||
terraform import 'dynu_dns_record.edge_lan_ddnsgeek_com_a_19646062' '9695470/19646062'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685'
|
||||
else
|
||||
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_17017685' '9695470/17017685'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056'
|
||||
else
|
||||
terraform import 'dynu_dns_record.familytree_lan_ddnsgeek_com_a_19646056' '9695470/19646056'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463'
|
||||
else
|
||||
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_14682463' '9695470/14682463'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063'
|
||||
else
|
||||
terraform import 'dynu_dns_record.gitea_lan_ddnsgeek_com_a_19646063' '9695470/19646063'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061'
|
||||
else
|
||||
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_17439061' '9695470/17439061'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047'
|
||||
else
|
||||
terraform import 'dynu_dns_record.gotify_lan_ddnsgeek_com_a_19646047' '9695470/19646047'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762'
|
||||
else
|
||||
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_18113762' '9695470/18113762'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050'
|
||||
else
|
||||
terraform import 'dynu_dns_record.grafana_lan_ddnsgeek_com_a_19646050' '9695470/19646050'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198'
|
||||
else
|
||||
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_18562198' '9695470/18562198'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059'
|
||||
else
|
||||
terraform import 'dynu_dns_record.influxdb_lan_ddnsgeek_com_a_19646059' '9695470/19646059'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978'
|
||||
else
|
||||
terraform import 'dynu_dns_record.kuma_lan_ddnsgeek_com_a_17454978' '9695470/17454978'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.lan_ddnsgeek_com_soa_8299670'
|
||||
else
|
||||
terraform import 'dynu_dns_record.lan_ddnsgeek_com_soa_8299670' '9695470/8299670'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342'
|
||||
else
|
||||
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_17462342' '9695470/17462342'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051'
|
||||
else
|
||||
terraform import 'dynu_dns_record.monitor_kuma_lan_ddnsgeek_com_a_19646051' '9695470/19646051'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643'
|
||||
else
|
||||
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19232643' '9695470/19232643'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058'
|
||||
else
|
||||
terraform import 'dynu_dns_record.mtls_bridge_lan_ddnsgeek_com_a_19646058' '9695470/19646058'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260'
|
||||
else
|
||||
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_10453260' '9695470/10453260'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057'
|
||||
else
|
||||
terraform import 'dynu_dns_record.nextcloud_lan_ddnsgeek_com_a_19646057' '9695470/19646057'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230'
|
||||
else
|
||||
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19041230' '9695470/19041230'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053'
|
||||
else
|
||||
terraform import 'dynu_dns_record.node_red_lan_ddnsgeek_com_a_19646053' '9695470/19646053'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262'
|
||||
else
|
||||
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_10453262' '9695470/10453262'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049'
|
||||
else
|
||||
terraform import 'dynu_dns_record.passbolt_lan_ddnsgeek_com_a_19646049' '9695470/19646049'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810'
|
||||
else
|
||||
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_17458810' '9695470/17458810'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046'
|
||||
else
|
||||
terraform import 'dynu_dns_record.portainer_lan_ddnsgeek_com_a_19646046' '9695470/19646046'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311'
|
||||
else
|
||||
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_18483311' '9695470/18483311'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061'
|
||||
else
|
||||
terraform import 'dynu_dns_record.prometheus_lan_ddnsgeek_com_a_19646061' '9695470/19646061'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263'
|
||||
else
|
||||
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_10453263' '9695470/10453263'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055'
|
||||
else
|
||||
terraform import 'dynu_dns_record.searxng_lan_ddnsgeek_com_a_19646055' '9695470/19646055'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565'
|
||||
else
|
||||
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_15901565' '9695470/15901565'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052'
|
||||
else
|
||||
terraform import 'dynu_dns_record.shifts_lan_ddnsgeek_com_a_19646052' '9695470/19646052'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867'
|
||||
else
|
||||
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_17081867' '9695470/17081867'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060'
|
||||
else
|
||||
terraform import 'dynu_dns_record.stockfill_lan_ddnsgeek_com_a_19646060' '9695470/19646060'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240'
|
||||
else
|
||||
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_10453240' '9695470/10453240'
|
||||
fi
|
||||
|
||||
if terraform state show 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' >/dev/null 2>&1; then
|
||||
echo 'Skipping already imported: dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054'
|
||||
else
|
||||
terraform import 'dynu_dns_record.traefik_lan_ddnsgeek_com_a_19646054' '9695470/19646054'
|
||||
fi
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copy this file to imports.tf and adjust IDs after confirming the
|
||||
# published provider docs for import ID formats.
|
||||
# For dynu_domain, import ID is commonly the root domain name.
|
||||
|
||||
import {
|
||||
to = dynu_domain.lan_ddnsgeek_com
|
||||
id = var.dynu_root_domain
|
||||
}
|
||||
|
||||
# DNS record imports are intentionally examples only because the provider
|
||||
# requires explicit record_type/hostname in config before import.
|
||||
#
|
||||
# import {
|
||||
# to = dynu_dns_record.grafana_lan_ddnsgeek_com
|
||||
# id = var.dynu_record_import_id
|
||||
# }
|
||||
@@ -0,0 +1,3 @@
|
||||
data "dynu_dns_records" "root" {
|
||||
hostname = var.dynu_root_domain
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
output "dynu_domain" {
|
||||
description = "Primary Dynu domain represented by this Terraform root."
|
||||
value = local.dynu_domain
|
||||
}
|
||||
|
||||
output "dynu_dns_records_catalog" {
|
||||
description = "Documentation catalog of expected Dynu DNS records discovered from repo service exposure."
|
||||
value = local.dynu_dns_records_catalog
|
||||
}
|
||||
|
||||
output "dynu_dns_inventory" {
|
||||
description = "Documentation-friendly Dynu DNS inventory for export and merge into broader infrastructure docs."
|
||||
value = {
|
||||
provider = "dynu"
|
||||
domain = local.dynu_domain
|
||||
record_count = length(local.dynu_dns_records_catalog)
|
||||
records = local.dynu_dns_records_catalog
|
||||
}
|
||||
}
|
||||
|
||||
output "dynu_root_domain_id" {
|
||||
description = "Dynu numeric domain ID resolved from dynu_root_domain."
|
||||
value = data.dynu_dns_records.root.domain_id
|
||||
}
|
||||
|
||||
output "dynu_root_domain_name" {
|
||||
description = "Dynu root domain name resolved from dynu_root_domain."
|
||||
value = data.dynu_dns_records.root.domain_name
|
||||
}
|
||||
|
||||
output "dynu_dns_records" {
|
||||
description = "Full read-only DNS record inventory returned by Dynu."
|
||||
value = data.dynu_dns_records.root.records
|
||||
}
|
||||
|
||||
output "dynu_dns_hostnames" {
|
||||
description = "Sorted hostname list discovered for dynu_root_domain."
|
||||
value = sort(distinct([for record in data.dynu_dns_records.root.records : record.hostname]))
|
||||
}
|
||||
|
||||
output "dynu_dns_record_import_ids" {
|
||||
description = "Map of Dynu DNS record identity to provider import IDs in domain_id/record_id format."
|
||||
value = {
|
||||
for record in data.dynu_dns_records.root.records :
|
||||
format("%s/%s/%s", record.hostname, record.record_type, record.id) => format("%s/%s", record.domain_id, record.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
provider "dynu" {
|
||||
# Keep auth local-only; do not commit credentials.
|
||||
api_key = var.dynu_api_key
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
locals {
|
||||
dynu_dns_records_catalog_base = {
|
||||
auth = {
|
||||
hostname = "auth"
|
||||
service = "authelia"
|
||||
source = "core/authelia/docker-compose.yml"
|
||||
purpose = "Authentication portal"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
gitea = {
|
||||
hostname = "gitea"
|
||||
service = "gitea"
|
||||
source = "apps/gitea/docker-compose.yml"
|
||||
purpose = "Gitea service endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
gotify = {
|
||||
hostname = "gotify"
|
||||
service = "gotify"
|
||||
source = "monitoring/gotify/docker-compose.yml"
|
||||
purpose = "Gotify notifications"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
grafana = {
|
||||
hostname = "grafana"
|
||||
service = "grafana"
|
||||
source = "monitoring/grafana/docker-compose.yml"
|
||||
purpose = "Grafana monitoring UI"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
familytree = {
|
||||
hostname = "familytree"
|
||||
service = "gramps"
|
||||
source = "apps/gramps/docker-compose.yml"
|
||||
purpose = "Family tree application"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
influxdb = {
|
||||
hostname = "influxdb"
|
||||
service = "influxdb"
|
||||
source = "monitoring/influxdb/docker-compose.yml"
|
||||
purpose = "InfluxDB metrics endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
monitor_kuma = {
|
||||
hostname = "monitor-kuma"
|
||||
service = "uptime-kuma"
|
||||
source = "monitoring/uptime-kuma/docker-compose.yml"
|
||||
purpose = "Uptime Kuma monitoring UI"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
mtls_bridge = {
|
||||
hostname = "mtls-bridge"
|
||||
service = "mtls-bridge"
|
||||
source = "monitoring/mtls-bridge/docker-compose.yml"
|
||||
purpose = "mTLS bridge API"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
nextcloud = {
|
||||
hostname = "nextcloud"
|
||||
service = "nextcloud-webapp"
|
||||
source = "apps/nextcloud/docker-compose.yml"
|
||||
purpose = "Nextcloud service endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
node_red = {
|
||||
hostname = "node-red"
|
||||
service = "node-red"
|
||||
source = "monitoring/node-red/docker-compose.yml"
|
||||
purpose = "Node-RED automation UI/API"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
passbolt = {
|
||||
hostname = "passbolt"
|
||||
service = "passbolt-webapp"
|
||||
source = "apps/passbolt/docker-compose.yml"
|
||||
purpose = "Passbolt password management"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
portainer = {
|
||||
hostname = "portainer"
|
||||
service = "portainer"
|
||||
source = "monitoring/portainer/docker-compose.yml"
|
||||
purpose = "Portainer admin endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
prometheus = {
|
||||
hostname = "prometheus"
|
||||
service = "prometheus"
|
||||
source = "monitoring/prometheus/docker-compose.yml"
|
||||
purpose = "Prometheus metrics endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
searxng = {
|
||||
hostname = "searxng"
|
||||
service = "searxng"
|
||||
source = "apps/searxng/docker-compose.yml"
|
||||
purpose = "SearXNG search endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
traefik = {
|
||||
hostname = "traefik"
|
||||
service = "traefik"
|
||||
source = "core/traefik/docker-compose.yml"
|
||||
purpose = "Traefik dashboard/API endpoint"
|
||||
record_type = null
|
||||
ttl = null
|
||||
target = null
|
||||
proxied = null
|
||||
}
|
||||
}
|
||||
|
||||
dynu_dns_records_catalog = {
|
||||
for key, record in local.dynu_dns_records_catalog_base :
|
||||
key => merge(record, {
|
||||
fqdn = format("%s.%s", record.hostname, local.dynu_domain)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Terraform dynu_dns_record resources/import commands from Dynu inventory outputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve()
|
||||
TF_ROOT = SCRIPT_PATH.parents[1]
|
||||
GENERATED_DIR = TF_ROOT / "generated"
|
||||
TF_FILE = GENERATED_DIR / "dynu_dns_records.generated.tf"
|
||||
IMPORT_SCRIPT = GENERATED_DIR / "import-dynu-dns-records.sh"
|
||||
INVENTORY_FILE = GENERATED_DIR / "dynu_dns_records_inventory.json"
|
||||
DEFAULT_RECORDS_OUTPUT = "dynu_dns_records"
|
||||
REQUIRED_RECORD_FIELDS = ("id", "domain_id", "hostname", "record_type")
|
||||
|
||||
HEADER_TF = """# ---------------------------------------------------------------------------
|
||||
# GENERATED FILE - REVIEW BEFORE USE
|
||||
#
|
||||
# Generated from Dynu brownfield DNS inventory.
|
||||
# Do not blindly apply this file to production DNS.
|
||||
# Import records into Terraform state before allowing Terraform to manage them.
|
||||
# ---------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
HEADER_SH = """#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# GENERATED FILE - REVIEW BEFORE USE
|
||||
#
|
||||
# Imports existing Dynu DNS records into Terraform state.
|
||||
# Does not apply changes.
|
||||
# ---------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TF_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${TF_ROOT}"
|
||||
|
||||
# Re-running imports will fail for resources already in state.
|
||||
# This script skips imports when state already contains the resource address.
|
||||
"""
|
||||
|
||||
OPTIONAL_FIELDS = ["group", "host", "priority", "weight", "port", "flags", "tag", "value", "node_name"]
|
||||
|
||||
|
||||
def run_terraform_output() -> dict:
|
||||
if not (TF_ROOT / ".terraform").exists():
|
||||
raise RuntimeError("Terraform is not initialized in infrastructure/terraform/dynu. Run: terraform init")
|
||||
|
||||
cmd = ["terraform", "output", "-json"]
|
||||
proc = subprocess.run(cmd, cwd=TF_ROOT, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to run {' '.join(cmd)}:\n{proc.stderr.strip()}")
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def type_shape_name(value: object) -> str:
|
||||
if isinstance(value, list):
|
||||
return "list"
|
||||
if isinstance(value, dict):
|
||||
return "object"
|
||||
return type(value).__name__
|
||||
|
||||
|
||||
def extract_records(payload: object, output_name: str) -> list[dict]:
|
||||
source = payload
|
||||
if isinstance(payload, list):
|
||||
source = payload
|
||||
elif isinstance(payload, dict):
|
||||
if isinstance(payload.get("value"), list):
|
||||
source = payload["value"]
|
||||
elif isinstance(payload.get("records"), list):
|
||||
source = payload["records"]
|
||||
elif isinstance(payload.get("value"), dict) and isinstance(payload["value"].get("records"), list):
|
||||
source = payload["value"]["records"]
|
||||
elif output_name in payload and isinstance(payload[output_name], dict):
|
||||
output_wrapper = payload[output_name]
|
||||
if isinstance(output_wrapper.get("value"), list):
|
||||
source = output_wrapper["value"]
|
||||
elif isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
|
||||
source = output_wrapper["value"]["records"]
|
||||
elif isinstance(output_wrapper.get("records"), list):
|
||||
source = output_wrapper["records"]
|
||||
else:
|
||||
raise RuntimeError(f"Output '{output_name}' does not contain a records list.")
|
||||
else:
|
||||
raise RuntimeError(f"Output '{output_name}' not found and no records list discovered.")
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported JSON payload type: {type(payload).__name__}")
|
||||
|
||||
if not isinstance(source, list):
|
||||
raise RuntimeError(f"Output '{output_name}' did not resolve to a list of records.")
|
||||
return source
|
||||
|
||||
|
||||
def validate_records(records: list[dict], output_name: str) -> None:
|
||||
for i, record in enumerate(records):
|
||||
if not isinstance(record, dict):
|
||||
raise RuntimeError(f"Selected output '{output_name}' has non-object record at index {i}: {type(record).__name__}.")
|
||||
missing = [field for field in REQUIRED_RECORD_FIELDS if field not in record]
|
||||
if missing:
|
||||
missing_text = ", ".join(missing)
|
||||
raise RuntimeError(
|
||||
f"Selected output '{output_name}' contains records, but they are not importable Dynu provider records. "
|
||||
f"Record #{i} is missing required fields: {missing_text}. "
|
||||
"Choose an output sourced from data.dynu_dns_records.root, such as dynu_dns_records or dynu_dns_inventory."
|
||||
)
|
||||
|
||||
|
||||
def describe_output(output_name: str, output_wrapper: object, full_outputs: dict) -> dict:
|
||||
details = {
|
||||
"name": output_name,
|
||||
"usable": False,
|
||||
"shape": type_shape_name(output_wrapper),
|
||||
"record_count": "none",
|
||||
"error": "no records list found",
|
||||
}
|
||||
if isinstance(output_wrapper, dict) and "value" in output_wrapper:
|
||||
details["shape"] = type_shape_name(output_wrapper.get("value"))
|
||||
|
||||
try:
|
||||
records = extract_records(full_outputs, output_name)
|
||||
except RuntimeError as exc:
|
||||
details["error"] = str(exc)
|
||||
return details
|
||||
|
||||
details["record_count"] = len(records)
|
||||
try:
|
||||
validate_records(records, output_name)
|
||||
except RuntimeError as exc:
|
||||
details["error"] = str(exc)
|
||||
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
|
||||
details["shape"] = "object with records list"
|
||||
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
|
||||
details["shape"] = "list"
|
||||
return details
|
||||
|
||||
details["usable"] = True
|
||||
details["error"] = None
|
||||
if isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), dict) and isinstance(output_wrapper["value"].get("records"), list):
|
||||
details["shape"] = "object with records list"
|
||||
elif isinstance(output_wrapper, dict) and isinstance(output_wrapper.get("value"), list):
|
||||
details["shape"] = "list"
|
||||
return details
|
||||
|
||||
|
||||
def choose_output_interactively(outputs: dict, descriptions: list[dict]) -> str | None:
|
||||
print("\nAvailable Terraform outputs:\n")
|
||||
indexed = {str(i): item for i, item in enumerate(descriptions, 1)}
|
||||
by_name = {item["name"]: item for item in descriptions}
|
||||
|
||||
for i, item in enumerate(descriptions, 1):
|
||||
print(f" {i}) {item['name']}")
|
||||
print(f" usable: {'yes' if item['usable'] else 'no'}")
|
||||
print(f" shape: {item['shape']}")
|
||||
print(f" record count: {item['record_count']}")
|
||||
if item["error"]:
|
||||
print(f" reason: {item['error']}")
|
||||
print()
|
||||
|
||||
attempts = 0
|
||||
while attempts < 3:
|
||||
attempts += 1
|
||||
try:
|
||||
selection = input(f"Choose an output to use for DNS records [1-{len(descriptions)}], or press Enter to cancel: ").strip()
|
||||
except KeyboardInterrupt:
|
||||
print("\nSelection cancelled.")
|
||||
return None
|
||||
|
||||
if selection == "":
|
||||
print("Selection cancelled.")
|
||||
return None
|
||||
|
||||
candidate = indexed.get(selection) or by_name.get(selection)
|
||||
if candidate is None:
|
||||
print("Invalid selection. Enter a number from the list or an exact output name.")
|
||||
continue
|
||||
|
||||
if not candidate["usable"]:
|
||||
print(f"Output '{candidate['name']}' is not usable: {candidate['error']}")
|
||||
continue
|
||||
|
||||
return candidate["name"]
|
||||
|
||||
raise RuntimeError("Too many invalid selections. Exiting without writing files.")
|
||||
|
||||
|
||||
def tf_name(record: dict) -> str:
|
||||
base = f"{record.get('hostname', '')}_{record.get('record_type', '')}_{record.get('id', '')}".lower()
|
||||
base = base.replace("*", "wildcard")
|
||||
base = re.sub(r"[^a-z0-9_]+", "_", base)
|
||||
base = re.sub(r"_+", "_", base).strip("_")
|
||||
if not base or not re.match(r"^[a-z]", base):
|
||||
base = f"record_{base}" if base else "record"
|
||||
if not base.endswith(str(record.get("id", ""))):
|
||||
base = f"{base}_{record.get('id', '')}"
|
||||
return base
|
||||
|
||||
|
||||
def hcl_value(value):
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
return json.dumps(value)
|
||||
|
||||
|
||||
def generate_resources(records: list[dict]) -> str:
|
||||
chunks = [HEADER_TF.rstrip(), ""]
|
||||
for rec in records:
|
||||
name = tf_name(rec)
|
||||
lines = [f'resource "dynu_dns_record" "{name}" {{']
|
||||
lines.append(f" hostname = {hcl_value(rec.get('hostname'))}")
|
||||
lines.append(f" record_type = {hcl_value(rec.get('record_type'))}")
|
||||
if rec.get("ttl") is not None:
|
||||
lines.append(f" ttl = {hcl_value(rec.get('ttl'))}")
|
||||
enabled = rec.get("enabled")
|
||||
if enabled is None:
|
||||
enabled = rec.get("state")
|
||||
if enabled is not None:
|
||||
lines.append(f" enabled = {hcl_value(enabled)}")
|
||||
|
||||
content = rec.get("content")
|
||||
rtype = str(rec.get("record_type", "")).upper()
|
||||
if content in (None, "") and rtype in {"A", "AAAA"}:
|
||||
lines.append(" dynamic = true")
|
||||
elif content not in (None, ""):
|
||||
lines.append(f" content = {hcl_value(content)}")
|
||||
|
||||
for field in OPTIONAL_FIELDS:
|
||||
value = rec.get(field)
|
||||
if value not in (None, ""):
|
||||
lines.append(f" {field.ljust(11)}= {hcl_value(value)}")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
" lifecycle {",
|
||||
" prevent_destroy = true",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
])
|
||||
chunks.extend(lines)
|
||||
return "\n".join(chunks).rstrip() + "\n"
|
||||
|
||||
|
||||
def generate_import_script(records: list[dict]) -> str:
|
||||
lines = [HEADER_SH.rstrip(), ""]
|
||||
for rec in records:
|
||||
name = tf_name(rec)
|
||||
import_id = f"{rec['domain_id']}/{rec['id']}"
|
||||
addr = f"dynu_dns_record.{name}"
|
||||
lines.append(f"if terraform state show '{addr}' >/dev/null 2>&1; then")
|
||||
lines.append(f" echo 'Skipping already imported: {addr}'")
|
||||
lines.append("else")
|
||||
lines.append(f" terraform import '{addr}' '{import_id}'")
|
||||
lines.append("fi")
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip() + "\n"
|
||||
|
||||
|
||||
def write_file(path: Path, content: str, dry_run: bool, overwrite: bool) -> None:
|
||||
if path.exists() and not overwrite:
|
||||
raise RuntimeError(f"Refusing to overwrite existing file: {path}. Re-run with --overwrite.")
|
||||
if dry_run:
|
||||
print(f"[dry-run] Would write {path}")
|
||||
return
|
||||
path.write_text(content, encoding="utf-8")
|
||||
print(f"Wrote {path}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print intended output paths without writing files.")
|
||||
parser.add_argument("--overwrite", "--force", action="store_true", dest="overwrite", help="Overwrite existing generated files.")
|
||||
parser.add_argument("--from-file", type=Path, help="Load inventory JSON from a file instead of calling terraform output.")
|
||||
parser.add_argument(
|
||||
"--records-output",
|
||||
default=None,
|
||||
help=(
|
||||
"Terraform output name containing Dynu DNS records. "
|
||||
f"Defaults to {DEFAULT_RECORDS_OUTPUT}; if missing in an interactive terminal, "
|
||||
"the script prompts you to choose from available outputs."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--no-interactive", action="store_true", help="Disable interactive output selection.")
|
||||
args = parser.parse_args()
|
||||
|
||||
records_output_explicit = args.records_output is not None
|
||||
records_output = args.records_output or DEFAULT_RECORDS_OUTPUT
|
||||
|
||||
try:
|
||||
payload = json.loads(args.from_file.read_text(encoding="utf-8")) if args.from_file else run_terraform_output()
|
||||
|
||||
selected_output = records_output
|
||||
descriptions: list[dict] = []
|
||||
if isinstance(payload, dict):
|
||||
descriptions = [describe_output(name, payload[name], payload) for name in sorted(payload)]
|
||||
|
||||
try:
|
||||
records = extract_records(payload, selected_output)
|
||||
validate_records(records, selected_output)
|
||||
except RuntimeError as exc:
|
||||
is_interactive = sys.stdin.isatty() and not args.no_interactive
|
||||
should_prompt = isinstance(payload, dict) and not records_output_explicit and is_interactive
|
||||
if should_prompt:
|
||||
print(f"Terraform output '{selected_output}' was not found or is unusable.\n")
|
||||
chosen = choose_output_interactively(payload, descriptions)
|
||||
if chosen is None:
|
||||
print("Exiting without writing files.")
|
||||
return 1
|
||||
selected_output = chosen
|
||||
records = extract_records(payload, selected_output)
|
||||
validate_records(records, selected_output)
|
||||
else:
|
||||
if isinstance(payload, dict):
|
||||
available = ", ".join(sorted(payload.keys())) or "(none)"
|
||||
if records_output_explicit:
|
||||
raise RuntimeError(
|
||||
f"Missing or unusable Terraform output '{selected_output}'. "
|
||||
f"Available outputs: {available}. Details: {exc}"
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Missing or unusable Terraform output '{selected_output}'. "
|
||||
f"Available outputs: {available}.\n\n"
|
||||
"Run interactively to choose an output, or pass one explicitly, for example:\n\n"
|
||||
" python3 scripts/generate-brownfield-records.py --records-output dynu_dns_inventory --dry-run"
|
||||
)
|
||||
raise
|
||||
|
||||
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
write_file(INVENTORY_FILE, json.dumps(records, indent=2, sort_keys=True) + "\n", args.dry_run, args.overwrite)
|
||||
write_file(TF_FILE, generate_resources(records), args.dry_run, args.overwrite)
|
||||
write_file(IMPORT_SCRIPT, generate_import_script(records), args.dry_run, args.overwrite)
|
||||
if not args.dry_run:
|
||||
IMPORT_SCRIPT.chmod(0o755)
|
||||
return 0
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
# Local-only credentials. Do not commit real values.
|
||||
dynu_api_key = "replace-with-dynu-api-key"
|
||||
dynu_username = null
|
||||
dynu_password = null
|
||||
|
||||
dynu_root_domain = "lan.ddnsgeek.com"
|
||||
dynu_record_import_id = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
variable "dynu_root_domain" {
|
||||
description = "Dynu root domain name to reconcile/import (for example: lan.ddnsgeek.com)."
|
||||
type = string
|
||||
default = "lan.ddnsgeek.com"
|
||||
}
|
||||
|
||||
variable "dynu_api_key" {
|
||||
description = "Dynu API key/token used by the Dynu Terraform provider."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "dynu_username" {
|
||||
description = "Optional Dynu username, only if required by the provider."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "dynu_password" {
|
||||
description = "Optional Dynu password, only if required by the provider."
|
||||
type = string
|
||||
sensitive = true
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "dynu_record_import_id" {
|
||||
description = "Placeholder import ID for a single dynu_dns_record during one-at-a-time reconciliation."
|
||||
type = string
|
||||
default = "REPLACE_WITH_DYNU_RECORD_IMPORT_ID"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
dynu = {
|
||||
source = "beatz174-bit/dynu"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# Terraform Docker Mirror Layer
|
||||
|
||||
This directory tracks selected existing Docker containers in Terraform for inventory/documentation purposes.
|
||||
|
||||
## Purpose
|
||||
|
||||
- Mirror specific running containers as Terraform resources.
|
||||
- Reconcile imported state into maintainable code.
|
||||
- Produce structured outputs/reminders that support documentation workflows.
|
||||
|
||||
## Boundary with Docker Compose
|
||||
|
||||
Docker Compose + `services-up.sh` remain runtime composition authority.
|
||||
|
||||
Terraform resources here are **not** the primary day-to-day deployment mechanism for app services.
|
||||
|
||||
## Current contents
|
||||
|
||||
- `main.tf` — import-first workflow notes and minimal scaffolding.
|
||||
- `searxng-webapp.tf` — generated/reconciled example container resource.
|
||||
- `outputs.tf` — documentation-oriented reminders/outputs.
|
||||
- `terraform.tfvars.example` — safe template for local values.
|
||||
|
||||
## Import/reconciliation workflow
|
||||
|
||||
1. Start with one existing container.
|
||||
2. Import with `import {}` block or `terraform import`.
|
||||
3. Inspect state / generated config.
|
||||
4. Reduce generated attributes to meaningful, stable arguments.
|
||||
5. Keep lifecycle `ignore_changes` narrow and justified.
|
||||
6. Iterate until plan is clean for the intended resource.
|
||||
|
||||
## Guardrails
|
||||
|
||||
- Do not attempt to mirror all containers in one pass.
|
||||
- Do not commit local state or real credentials.
|
||||
- Treat generated config as draft input that needs review.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [../README.md](../README.md)
|
||||
- [../../../docs/source-of-truth.md](../../../docs/source-of-truth.md)
|
||||
- [../../../docs/terraform-workflows.md](../../../docs/terraform-workflows.md)
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "authelia" {
|
||||
name = local.docker_containers["authelia"].container_name
|
||||
image = local.docker_containers["authelia"].image
|
||||
|
||||
restart = local.docker_containers["authelia"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
locals {
|
||||
docker_containers = {
|
||||
"authelia" = {
|
||||
terraform_resource = "docker_container.authelia"
|
||||
compose_project = "core"
|
||||
compose_service = "authelia"
|
||||
compose_file = "core/authelia/docker-compose.yml"
|
||||
container_name = "authelia"
|
||||
image = "authelia/authelia"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/core/authelia->/config"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/core/authelia"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.middlewares.authelia.forwardauth.address" = "http://authelia:9091/api/verify?rd=https://auth.lan.ddnsgeek.com/"
|
||||
"traefik.http.middlewares.authelia.forwardauth.authResponseHeaders" = "Remote-User,Remote-Groups"
|
||||
"traefik.http.middlewares.authelia.forwardauth.maxResponseBodySize" = "2097152"
|
||||
"traefik.http.middlewares.authelia.forwardauth.trustForwardHeader" = "true"
|
||||
"traefik.http.routers.authelia.entrypoints" = "websecure"
|
||||
"traefik.http.routers.authelia.rule" = "Host(`auth.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.authelia.tls" = "true"
|
||||
"traefik.http.routers.authelia.tls.certresolver" = "myresolver"
|
||||
}
|
||||
}
|
||||
"crowdsec" = {
|
||||
terraform_resource = "docker_container.crowdsec"
|
||||
compose_project = "core"
|
||||
compose_service = "crowdsec"
|
||||
compose_file = "core/crowdsec/docker-compose.yml"
|
||||
container_name = "crowdsec"
|
||||
image = "core-crowdsec"
|
||||
image_source = "compose_build_inferred"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/core/crowdsec/logs->/logs:ro", "bind:/home/nixos/docker/core/crowdsec/data->/var/lib/crowdsec/data", "bind:/home/nixos/docker/core/crowdsec/config->/etc/crowdsec"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/core/crowdsec"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {}
|
||||
}
|
||||
"docker-socket-proxy" = {
|
||||
terraform_resource = "docker_container.docker_socket_proxy"
|
||||
compose_project = "core"
|
||||
compose_service = "docker-socket-proxy"
|
||||
compose_file = "monitoring/docker-socket-proxy/docker-compose.yml"
|
||||
container_name = "docker-socket-proxy"
|
||||
image = "tecnativa/docker-socket-proxy:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/var/run/docker.sock->/var/run/docker.sock:ro"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"docker-update-exporter" = {
|
||||
terraform_resource = "docker_container.docker_update_exporter"
|
||||
compose_project = "core"
|
||||
compose_service = "docker-update-exporter"
|
||||
compose_file = "monitoring/docker-exporter/docker-compose.yml"
|
||||
container_name = "docker-update-exporter"
|
||||
image = "core-docker-update-exporter"
|
||||
image_source = "compose_build_inferred"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor"]
|
||||
mounts = ["bind:/root/.docker/config.json->/root/.docker/config.json:ro", "bind:/home/nixos/docker/monitoring/docker-exporter/data->/data", "bind:/home/nixos/docker->/compose:ro"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/monitoring/docker-exporter"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {}
|
||||
}
|
||||
"error-pages" = {
|
||||
terraform_resource = "docker_container.error_pages"
|
||||
compose_project = "core"
|
||||
compose_service = "error-pages"
|
||||
compose_file = "core/error-pages/docker-compose.yml"
|
||||
container_name = "error-pages"
|
||||
image = "tarampampam/error-pages:3"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = []
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.middlewares.error-pages-middleware.errors.query" = "/{status}.html"
|
||||
"traefik.http.middlewares.error-pages-middleware.errors.service" = "error-pages-service"
|
||||
"traefik.http.middlewares.error-pages-middleware.errors.status" = "400-599"
|
||||
"traefik.http.routers.error-pages-router.entrypoints" = "web"
|
||||
"traefik.http.routers.error-pages-router.middlewares" = "error-pages-middleware"
|
||||
"traefik.http.routers.error-pages-router.rule" = "HostRegexp(`{host:.+}`)"
|
||||
"traefik.http.services.error-pages-service.loadbalancer.server.port" = "8080"
|
||||
}
|
||||
}
|
||||
"gitea" = {
|
||||
terraform_resource = "docker_container.gitea"
|
||||
compose_project = "core"
|
||||
compose_service = "gitea"
|
||||
compose_file = "apps/gitea/docker-compose.yml"
|
||||
container_name = "gitea"
|
||||
image = "gitea/gitea:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/gitea/data->/data"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.gitea.entrypoints" = "websecure"
|
||||
"traefik.http.routers.gitea.rule" = "Host(`gitea.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.gitea.tls" = "true"
|
||||
"traefik.http.routers.gitea.tls.certresolver" = "myresolver"
|
||||
"traefik.http.services.gitea.loadbalancer.server.port" = "3000"
|
||||
}
|
||||
}
|
||||
"gotify" = {
|
||||
terraform_resource = "docker_container.gotify"
|
||||
compose_project = "core"
|
||||
compose_service = "gotify"
|
||||
compose_file = "monitoring/gotify/docker-compose.yml"
|
||||
container_name = "gotify"
|
||||
image = "gotify/server:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/gotify/data->/app/data"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.gotify.entrypoints" = "websecure"
|
||||
"traefik.http.routers.gotify.rule" = "Host(`gotify.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.gotify.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.gotify.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.gotify.loadbalancer.server.port" = "80"
|
||||
}
|
||||
}
|
||||
"grafana" = {
|
||||
terraform_resource = "docker_container.grafana"
|
||||
compose_project = "core"
|
||||
compose_service = "grafana"
|
||||
compose_file = "monitoring/grafana/docker-compose.yml"
|
||||
container_name = "grafana"
|
||||
image = "grafana/grafana:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/grafana/data->/var/lib/grafana"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.grafana.entrypoints" = "websecure"
|
||||
"traefik.http.routers.grafana.rule" = "Host(`grafana.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.grafana.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.grafana.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.grafana.loadbalancer.server.port" = "3000"
|
||||
}
|
||||
}
|
||||
"gramps-redis" = {
|
||||
terraform_resource = "docker_container.gramps_redis"
|
||||
compose_project = "core"
|
||||
compose_service = "gramps-redis"
|
||||
compose_file = "apps/gramps/docker-compose.yml"
|
||||
container_name = "gramps-redis"
|
||||
image = "valkey/valkey:8-alpine"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["gramps"]
|
||||
mounts = []
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"gramps-web" = {
|
||||
terraform_resource = "docker_container.gramps_web"
|
||||
compose_project = "core"
|
||||
compose_service = "grampsweb"
|
||||
compose_file = "apps/gramps/docker-compose.yml"
|
||||
container_name = "gramps-web"
|
||||
image = "ghcr.io/gramps-project/grampsweb:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["gramps", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/gramps/data/users->/app/users", "bind:/home/nixos/docker/apps/gramps/data/index->/app/indexdir", "bind:/home/nixos/docker/apps/gramps/data/thumbnail_cache->/app/thumbnail_cache", "bind:/home/nixos/docker/apps/gramps/data/cache->/app/cache", "bind:/home/nixos/docker/apps/gramps/data/secret->/app/secret", "bind:/home/nixos/docker/apps/gramps/data/db->/root/.gramps/grampsdb", "bind:/home/nixos/docker/apps/gramps/data/media->/app/media", "bind:/home/nixos/docker/apps/gramps/data/tmp->/tmp"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.gramps.entrypoints" = "websecure"
|
||||
"traefik.http.routers.gramps.rule" = "Host(`familytree.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.gramps.tls.certresolver" = "myresolver"
|
||||
"traefik.http.services.gramps.loadbalancer.server.port" = "5000"
|
||||
}
|
||||
}
|
||||
"gramps-web-celery" = {
|
||||
terraform_resource = "docker_container.gramps_web_celery"
|
||||
compose_project = "core"
|
||||
compose_service = "grampsweb_celery"
|
||||
compose_file = "apps/gramps/docker-compose.yml"
|
||||
container_name = "gramps-web-celery"
|
||||
image = "ghcr.io/gramps-project/grampsweb:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["gramps"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/gramps/data/users->/app/users", "bind:/home/nixos/docker/apps/gramps/data/index->/app/indexdir", "bind:/home/nixos/docker/apps/gramps/data/thumbnail_cache->/app/thumbnail_cache", "bind:/home/nixos/docker/apps/gramps/data/cache->/app/cache", "bind:/home/nixos/docker/apps/gramps/data/secret->/app/secret", "bind:/home/nixos/docker/apps/gramps/data/db->/root/.gramps/grampsdb", "bind:/home/nixos/docker/apps/gramps/data/media->/app/media", "bind:/home/nixos/docker/apps/gramps/data/tmp->/tmp"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"influxdb" = {
|
||||
terraform_resource = "docker_container.influxdb"
|
||||
compose_project = "core"
|
||||
compose_service = "influxdb"
|
||||
compose_file = "monitoring/influxdb/docker-compose.yml"
|
||||
container_name = "influxdb"
|
||||
image = "influxdb:2.7"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/influxdb->/var/lib/influxdb2"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.influxdb.entrypoints" = "websecure"
|
||||
"traefik.http.routers.influxdb.middlewares" = "authelia"
|
||||
"traefik.http.routers.influxdb.rule" = "Host(`influxdb.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.influxdb.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.influxdb.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.influxdb.loadbalancer.server.port" = "8086"
|
||||
}
|
||||
}
|
||||
"monitor-kuma" = {
|
||||
terraform_resource = "docker_container.monitor_kuma"
|
||||
compose_project = "core"
|
||||
compose_service = "monitor-kuma"
|
||||
compose_file = "monitoring/uptime-kuma/docker-compose.yml"
|
||||
container_name = "monitor-kuma"
|
||||
image = "louislam/uptime-kuma:2.1.1"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/uptime-kuma/data->/app/data"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.monitor.entrypoints" = "websecure"
|
||||
"traefik.http.routers.monitor.rule" = "Host(`monitor-kuma.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.monitor.tls" = "true"
|
||||
"traefik.http.routers.monitor.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.monitor.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.monitor.loadbalancer.server.port" = "3001"
|
||||
}
|
||||
}
|
||||
"mtls-bridge" = {
|
||||
terraform_resource = "docker_container.mtls_bridge"
|
||||
compose_project = "core"
|
||||
compose_service = "mtls-bridge"
|
||||
compose_file = "monitoring/mtls-bridge/docker-compose.yml"
|
||||
container_name = "mtls-bridge"
|
||||
image = "core-mtls-bridge"
|
||||
image_source = "compose_build_inferred"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/core/traefik/certs->/certs:ro"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/monitoring/mtls-bridge"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.middlewares.mtls-bridge-auth.basicauth.users" = ""
|
||||
"traefik.http.middlewares.mtls-bridge-cors.headers.accesscontrolallowcredentials" = "true"
|
||||
"traefik.http.middlewares.mtls-bridge-cors.headers.accesscontrolallowheaders" = "authorization,content-type,x-grafana-action,x-grafana-device-id"
|
||||
"traefik.http.middlewares.mtls-bridge-cors.headers.accesscontrolallowmethods" = "GET,POST,PUT,PATCH,DELETE,OPTIONS"
|
||||
"traefik.http.middlewares.mtls-bridge-cors.headers.accesscontrolalloworiginlist" = "https://grafana.lan.ddnsgeek.com"
|
||||
"traefik.http.middlewares.mtls-bridge-cors.headers.addvaryheader" = "true"
|
||||
"traefik.http.routers.mtls-bridge-preflight.entrypoints" = "websecure"
|
||||
"traefik.http.routers.mtls-bridge-preflight.middlewares" = "mtls-bridge-cors"
|
||||
"traefik.http.routers.mtls-bridge-preflight.priority" = "100"
|
||||
"traefik.http.routers.mtls-bridge-preflight.rule" = "Host(`mtls-bridge.lan.ddnsgeek.com`) && Method(`OPTIONS`)"
|
||||
"traefik.http.routers.mtls-bridge-preflight.service" = "mtls-bridge"
|
||||
"traefik.http.routers.mtls-bridge-preflight.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.mtls-bridge.entrypoints" = "websecure"
|
||||
"traefik.http.routers.mtls-bridge.middlewares" = "mtls-bridge-auth,mtls-bridge-cors"
|
||||
"traefik.http.routers.mtls-bridge.rule" = "Host(`mtls-bridge.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.mtls-bridge.tls.certresolver" = "myresolver"
|
||||
"traefik.http.services.mtls-bridge.loadbalancer.server.port" = "8080"
|
||||
}
|
||||
}
|
||||
"nextcloud-db" = {
|
||||
terraform_resource = "docker_container.nextcloud_db"
|
||||
compose_project = "core"
|
||||
compose_service = "nextcloud-db"
|
||||
compose_file = "apps/nextcloud/docker-compose.yml"
|
||||
container_name = "nextcloud-db"
|
||||
image = "mariadb:11.4"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["nextcloud"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/nextcloud/database->/var/lib/mysql"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"nextcloud-redis" = {
|
||||
terraform_resource = "docker_container.nextcloud_redis"
|
||||
compose_project = "core"
|
||||
compose_service = "nextcloud-redis"
|
||||
compose_file = "apps/nextcloud/docker-compose.yml"
|
||||
container_name = "nextcloud-redis"
|
||||
image = "redis"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["nextcloud"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/nextcloud/data/redis->/data"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"nextcloud-webapp" = {
|
||||
terraform_resource = "docker_container.nextcloud_webapp"
|
||||
compose_project = "core"
|
||||
compose_service = "nextcloud-webapp"
|
||||
compose_file = "apps/nextcloud/docker-compose.yml"
|
||||
container_name = "nextcloud-webapp"
|
||||
image = "core-nextcloud-webapp"
|
||||
image_source = "compose_build_inferred"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["nextcloud", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/nextcloud/data->/var/www/html/data", "bind:/home/nixos/docker/apps/nextcloud/config->/var/www/html/config", "tmpfs:->/tmp:exec"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/apps/nextcloud"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.middlewares.nextcloud-dav.replacepathregex.regex" = "^/.well-known/ca(l|rd)dav"
|
||||
"traefik.http.middlewares.nextcloud-dav.replacepathregex.replacement" = "/remote.php/dav/"
|
||||
"traefik.http.middlewares.nextcloud-nodeinfo.replacepathregex.regex" = "^/.well-known/nodeinfo"
|
||||
"traefik.http.middlewares.nextcloud-nodeinfo.replacepathregex.replacement" = "/nextcloud/index.php/.well-known/nodeinfo/"
|
||||
"traefik.http.middlewares.nextcloud-webfinger.redirectregex.permanent" = "true"
|
||||
"traefik.http.middlewares.nextcloud-webfinger.redirectregex.regex" = "https://(.*)/.well-known/webfinger"
|
||||
"traefik.http.middlewares.nextcloud-webfinger.redirectregex.replacement" = "https://$${1}/nextcloud/index.php/.well-known/webfinger"
|
||||
"traefik.http.routers.nextcloud.entrypoints" = "websecure"
|
||||
"traefik.http.routers.nextcloud.middlewares" = "nextcloud-dav, nextcloud-webfinger"
|
||||
"traefik.http.routers.nextcloud.rule" = "Host(`nextcloud.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.nextcloud.tls.certresolver" = "myresolver"
|
||||
}
|
||||
}
|
||||
"node-exporter" = {
|
||||
terraform_resource = "docker_container.node_exporter"
|
||||
compose_project = "core"
|
||||
compose_service = "node-exporter"
|
||||
compose_file = "monitoring/node-exporter/docker-compose.yml"
|
||||
container_name = "node-exporter"
|
||||
image = "prom/node-exporter:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor"]
|
||||
mounts = ["bind:/proc->/host/proc:ro", "bind:/sys->/host/sys:ro", "bind:/->/rootfs:ro"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"node-red" = {
|
||||
terraform_resource = "docker_container.node_red"
|
||||
compose_project = "core"
|
||||
compose_service = "node-red"
|
||||
compose_file = "monitoring/node-red/docker-compose.yml"
|
||||
container_name = "node-red"
|
||||
image = "core-node-red"
|
||||
image_source = "compose_build_inferred"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/node-red/data->/data", "bind:/home/nixos/docker->/compose/docker:ro", "bind:/home/nixos/raspi->/compose/raspi:ro"]
|
||||
published_ports = []
|
||||
build_context = "/home/nixos/docker/monitoring/node-red"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.node-red.entrypoints" = "websecure"
|
||||
"traefik.http.routers.node-red.middlewares" = "authelia"
|
||||
"traefik.http.routers.node-red.rule" = "Host(`node-red.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.node-red.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.node-red.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.node-red.loadbalancer.server.port" = "1880"
|
||||
}
|
||||
}
|
||||
"passbolt-db" = {
|
||||
terraform_resource = "docker_container.passbolt_db"
|
||||
compose_project = "core"
|
||||
compose_service = "passbolt-db"
|
||||
compose_file = "apps/passbolt/docker-compose.yml"
|
||||
container_name = "passbolt-db"
|
||||
image = "mariadb:12"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["passbolt"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/passbolt/data/database->/var/lib/mysql"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"passbolt-webapp" = {
|
||||
terraform_resource = "docker_container.passbolt_webapp"
|
||||
compose_project = "core"
|
||||
compose_service = "passbolt-webapp"
|
||||
compose_file = "apps/passbolt/docker-compose.yml"
|
||||
container_name = "passbolt-webapp"
|
||||
image = "passbolt/passbolt:latest-ce"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["passbolt", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/apps/passbolt/data/gpg->/etc/passbolt/gpg", "bind:/home/nixos/docker/apps/passbolt/data/jwt->/etc/passbolt/jwt"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.passbolt.entrypoints" = "websecure"
|
||||
"traefik.http.routers.passbolt.rule" = "Host(`passbolt.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.passbolt.tls.certresolver" = "myresolver"
|
||||
}
|
||||
}
|
||||
"pihole-exporter" = {
|
||||
terraform_resource = "docker_container.pihole_exporter"
|
||||
compose_project = "core"
|
||||
compose_service = "pihole-exporter"
|
||||
compose_file = "monitoring/pihole-exporter/docker-compose.yml"
|
||||
container_name = "pihole-exporter"
|
||||
image = "ekofr/pihole-exporter:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor"]
|
||||
mounts = []
|
||||
published_ports = ["9617:9617/tcp"]
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"portainer" = {
|
||||
terraform_resource = "docker_container.portainer"
|
||||
compose_project = "core"
|
||||
compose_service = "portainer"
|
||||
compose_file = "monitoring/portainer/docker-compose.yml"
|
||||
container_name = "portainer"
|
||||
image = "portainer/portainer-ce:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/portainer/data->/data"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.portainer.entrypoints" = "websecure"
|
||||
"traefik.http.routers.portainer.rule" = "Host(`portainer.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.portainer.tls" = "true"
|
||||
"traefik.http.routers.portainer.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.portainer.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.portainer.loadbalancer.server.port" = "9000"
|
||||
}
|
||||
}
|
||||
"prometheus" = {
|
||||
terraform_resource = "docker_container.prometheus"
|
||||
compose_project = "core"
|
||||
compose_service = "prometheus"
|
||||
compose_file = "monitoring/prometheus/docker-compose.yml"
|
||||
container_name = "prometheus"
|
||||
image = "prom/prometheus:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor", "traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/prometheus/prometheus.yml->/etc/prometheus/prometheus.yml:ro", "bind:/home/nixos/docker/monitoring/prometheus/data->/prometheus", "bind:/home/nixos/docker/monitoring/prometheus/rules->/etc/prometheus/rules:ro", "bind:/home/nixos/docker/secrets/prometheus_kuma_basic_auth_password.txt->/run/secrets/prometheus_kuma_basic_auth_password:ro"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.prometheus.entrypoints" = "websecure"
|
||||
"traefik.http.routers.prometheus.middlewares" = "authelia"
|
||||
"traefik.http.routers.prometheus.rule" = "Host(`prometheus.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.prometheus.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.prometheus.tls.options" = "mtls-private-admin@file"
|
||||
"traefik.http.services.prometheus.loadbalancer.server.port" = "9090"
|
||||
}
|
||||
}
|
||||
"searxng-webapp" = {
|
||||
terraform_resource = "docker_container.searxng-webapp"
|
||||
compose_project = "core"
|
||||
compose_service = "searxng-webapp"
|
||||
compose_file = "apps/searxng/docker-compose.yml"
|
||||
container_name = "searxng-webapp"
|
||||
image = "searxng/searxng"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = []
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.searxng.entrypoints" = "websecure"
|
||||
"traefik.http.routers.searxng.rule" = "Host(`searxng.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.searxng.tls.certresolver" = "myresolver"
|
||||
"traefik.http.services.searxng.loadbalancer.server.port" = "8080"
|
||||
}
|
||||
}
|
||||
"telegraf" = {
|
||||
terraform_resource = "docker_container.telegraf"
|
||||
compose_project = "core"
|
||||
compose_service = "telegraf"
|
||||
compose_file = "monitoring/telegraf/docker-compose.yml"
|
||||
container_name = "telegraf"
|
||||
image = "telegraf:latest"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "unless-stopped"
|
||||
network_mode = null
|
||||
networks = ["monitor"]
|
||||
mounts = ["bind:/home/nixos/docker/monitoring/telegraf/telegraf.conf->/etc/telegraf/telegraf.conf:ro", "bind:/home/nixos/docker/monitoring/node-red/data->/var/log/node-red:ro"]
|
||||
published_ports = []
|
||||
build_context = null
|
||||
build_dockerfile = null
|
||||
useful_labels = {}
|
||||
}
|
||||
"traefik" = {
|
||||
terraform_resource = "docker_container.traefik"
|
||||
compose_project = "core"
|
||||
compose_service = "traefik"
|
||||
compose_file = "core/traefik/docker-compose.yml"
|
||||
container_name = "traefik"
|
||||
image = "traefik:3"
|
||||
image_source = "declared_image"
|
||||
restart_policy = "always"
|
||||
network_mode = null
|
||||
networks = ["traefik"]
|
||||
mounts = ["bind:/home/nixos/docker/core/traefik/data/letsencrypt->/letsencrypt", "bind:/home/nixos/docker/core/traefik/data/logs->/logs", "bind:/home/nixos/docker/core/traefik/certs->/etc/traefik/certs:ro", "bind:/home/nixos/docker/core/traefik/dynamic.yml->/etc/traefik/dynamic.yml:ro", "bind:/home/nixos/docker/core/traefik/traefik.yml->/etc/traefik/traefik.yml:ro", "bind:/home/nixos/docker/core/traefik/data/plugins->/plugins-storage"]
|
||||
published_ports = ["80:80/tcp", "443:443/tcp"]
|
||||
build_context = "/home/nixos/docker/core"
|
||||
build_dockerfile = "Dockerfile"
|
||||
useful_labels = {
|
||||
"traefik.docker.network" = "core_traefik"
|
||||
"traefik.enable" = "true"
|
||||
"traefik.http.routers.traefik.entrypoints" = "websecure"
|
||||
"traefik.http.routers.traefik.middlewares" = "authelia"
|
||||
"traefik.http.routers.traefik.observability.tracing" = "true"
|
||||
"traefik.http.routers.traefik.rule" = "Host(`traefik.lan.ddnsgeek.com`)"
|
||||
"traefik.http.routers.traefik.service" = "api@internal"
|
||||
"traefik.http.routers.traefik.tls.certresolver" = "myresolver"
|
||||
"traefik.http.routers.traefik.tls.options" = "mtls-private-admin@file"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "crowdsec" {
|
||||
name = local.docker_containers["crowdsec"].container_name
|
||||
image = local.docker_containers["crowdsec"].image
|
||||
|
||||
restart = local.docker_containers["crowdsec"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "docker_socket_proxy" {
|
||||
name = local.docker_containers["docker-socket-proxy"].container_name
|
||||
image = local.docker_containers["docker-socket-proxy"].image
|
||||
|
||||
restart = local.docker_containers["docker-socket-proxy"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "docker_update_exporter" {
|
||||
name = local.docker_containers["docker-update-exporter"].container_name
|
||||
image = local.docker_containers["docker-update-exporter"].image
|
||||
|
||||
restart = local.docker_containers["docker-update-exporter"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "error_pages" {
|
||||
name = local.docker_containers["error-pages"].container_name
|
||||
image = local.docker_containers["error-pages"].image
|
||||
|
||||
restart = local.docker_containers["error-pages"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "gitea" {
|
||||
name = local.docker_containers["gitea"].container_name
|
||||
image = local.docker_containers["gitea"].image
|
||||
|
||||
restart = local.docker_containers["gitea"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "gotify" {
|
||||
name = local.docker_containers["gotify"].container_name
|
||||
image = local.docker_containers["gotify"].image
|
||||
|
||||
restart = local.docker_containers["gotify"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "grafana" {
|
||||
name = local.docker_containers["grafana"].container_name
|
||||
image = local.docker_containers["grafana"].image
|
||||
|
||||
restart = local.docker_containers["grafana"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "gramps_redis" {
|
||||
name = local.docker_containers["gramps-redis"].container_name
|
||||
image = local.docker_containers["gramps-redis"].image
|
||||
|
||||
restart = local.docker_containers["gramps-redis"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "gramps_web_celery" {
|
||||
name = local.docker_containers["gramps-web-celery"].container_name
|
||||
image = local.docker_containers["gramps-web-celery"].image
|
||||
|
||||
restart = local.docker_containers["gramps-web-celery"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "gramps_web" {
|
||||
name = local.docker_containers["gramps-web"].container_name
|
||||
image = local.docker_containers["gramps-web"].image
|
||||
|
||||
restart = local.docker_containers["gramps-web"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "influxdb" {
|
||||
name = local.docker_containers["influxdb"].container_name
|
||||
image = local.docker_containers["influxdb"].image
|
||||
|
||||
restart = local.docker_containers["influxdb"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Docker container resources are split into one file per container.
|
||||
# See container-catalog.tf for documentation-oriented metadata used by outputs.
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "monitor_kuma" {
|
||||
name = local.docker_containers["monitor-kuma"].container_name
|
||||
image = local.docker_containers["monitor-kuma"].image
|
||||
|
||||
restart = local.docker_containers["monitor-kuma"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "mtls_bridge" {
|
||||
name = local.docker_containers["mtls-bridge"].container_name
|
||||
image = local.docker_containers["mtls-bridge"].image
|
||||
|
||||
restart = local.docker_containers["mtls-bridge"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "nextcloud_db" {
|
||||
name = local.docker_containers["nextcloud-db"].container_name
|
||||
image = local.docker_containers["nextcloud-db"].image
|
||||
|
||||
restart = local.docker_containers["nextcloud-db"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "nextcloud_redis" {
|
||||
name = local.docker_containers["nextcloud-redis"].container_name
|
||||
image = local.docker_containers["nextcloud-redis"].image
|
||||
|
||||
restart = local.docker_containers["nextcloud-redis"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "nextcloud_webapp" {
|
||||
name = local.docker_containers["nextcloud-webapp"].container_name
|
||||
image = local.docker_containers["nextcloud-webapp"].image
|
||||
|
||||
restart = local.docker_containers["nextcloud-webapp"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "node_exporter" {
|
||||
name = local.docker_containers["node-exporter"].container_name
|
||||
image = local.docker_containers["node-exporter"].image
|
||||
|
||||
restart = local.docker_containers["node-exporter"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "node_red" {
|
||||
name = local.docker_containers["node-red"].container_name
|
||||
image = local.docker_containers["node-red"].image
|
||||
|
||||
restart = local.docker_containers["node-red"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
output "docker_host_in_use" {
|
||||
description = "Docker daemon endpoint currently targeted by this workspace."
|
||||
value = var.docker_host
|
||||
}
|
||||
|
||||
output "docker_containers" {
|
||||
description = "Documentation-shaped inventory of Docker containers managed via services-up.sh compose sources."
|
||||
value = local.docker_containers
|
||||
}
|
||||
|
||||
output "docker_inventory" {
|
||||
description = "Compact Docker inventory suitable for export and merging into broader infrastructure docs."
|
||||
value = {
|
||||
compose_project = "core"
|
||||
container_count = length(local.docker_containers)
|
||||
containers = {
|
||||
for key, container in local.docker_containers : key => {
|
||||
compose_service = container.compose_service
|
||||
compose_file = container.compose_file
|
||||
container_name = container.container_name
|
||||
image = container.image
|
||||
image_source = container.image_source
|
||||
build_context = container.build_context
|
||||
network_mode = container.network_mode
|
||||
networks = container.networks
|
||||
published_ports = container.published_ports
|
||||
mounts = container.mounts
|
||||
restart_policy = container.restart_policy
|
||||
labels = container.useful_labels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "managed_container_names" {
|
||||
description = "Names of containers intentionally tracked in Terraform documentation resources."
|
||||
value = sort(keys(local.docker_containers))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "passbolt_db" {
|
||||
name = local.docker_containers["passbolt-db"].container_name
|
||||
image = local.docker_containers["passbolt-db"].image
|
||||
|
||||
restart = local.docker_containers["passbolt-db"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "passbolt_webapp" {
|
||||
name = local.docker_containers["passbolt-webapp"].container_name
|
||||
image = local.docker_containers["passbolt-webapp"].image
|
||||
|
||||
restart = local.docker_containers["passbolt-webapp"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "pihole_exporter" {
|
||||
name = local.docker_containers["pihole-exporter"].container_name
|
||||
image = local.docker_containers["pihole-exporter"].image
|
||||
|
||||
restart = local.docker_containers["pihole-exporter"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "portainer" {
|
||||
name = local.docker_containers["portainer"].container_name
|
||||
image = local.docker_containers["portainer"].image
|
||||
|
||||
restart = local.docker_containers["portainer"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "prometheus" {
|
||||
name = local.docker_containers["prometheus"].container_name
|
||||
image = local.docker_containers["prometheus"].image
|
||||
|
||||
restart = local.docker_containers["prometheus"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
provider "docker" {
|
||||
# Local Docker socket default for incremental import/documentation workflow.
|
||||
host = var.docker_host
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "searxng-webapp" {
|
||||
name = local.docker_containers["searxng-webapp"].container_name
|
||||
image = local.docker_containers["searxng-webapp"].image
|
||||
|
||||
restart = local.docker_containers["searxng-webapp"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
resource "docker_container" "telegraf" {
|
||||
name = local.docker_containers["telegraf"].container_name
|
||||
image = local.docker_containers["telegraf"].image
|
||||
|
||||
restart = local.docker_containers["telegraf"].restart_policy
|
||||
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Example only. Copy to terraform.tfvars for local use if needed.
|
||||
# Do not commit host-specific overrides.
|
||||
|
||||
# docker_host = "unix:///var/run/docker.sock"
|
||||
|
||||
# Optional: list container names intentionally tracked in Terraform outputs.
|
||||
managed_container_names = [
|
||||
"example-container-name"
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
resource "docker_container" "traefik" {
|
||||
name = local.docker_containers["traefik"].container_name
|
||||
image = local.docker_containers["traefik"].image
|
||||
|
||||
restart = local.docker_containers["traefik"].restart_policy
|
||||
|
||||
network_mode = "core_traefik"
|
||||
|
||||
ports {
|
||||
internal = 80
|
||||
external = 80
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
ports {
|
||||
internal = 443
|
||||
external = 443
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/data/letsencrypt"
|
||||
target = "/letsencrypt"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/data/logs"
|
||||
target = "/logs"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/certs"
|
||||
target = "/etc/traefik/certs"
|
||||
read_only = true
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/dynamic.yml"
|
||||
target = "/etc/traefik/dynamic.yml"
|
||||
read_only = true
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/traefik.yml"
|
||||
target = "/etc/traefik/traefik.yml"
|
||||
read_only = true
|
||||
}
|
||||
|
||||
mounts {
|
||||
type = "bind"
|
||||
source = "/home/nixos/docker/core/traefik/data/plugins"
|
||||
target = "/plugins-storage"
|
||||
read_only = false
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
env,
|
||||
labels,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
variable "docker_host" {
|
||||
description = "Docker daemon host for local import workflow."
|
||||
type = string
|
||||
default = "unix:///var/run/docker.sock"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
version = "3.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# docker_container_placeholder module
|
||||
|
||||
Placeholder module directory for future shared Docker container patterns.
|
||||
|
||||
This is intentionally empty during initial import-first adoption.
|
||||
Keep per-container resources explicit in `../../docker/main.tf` until stable patterns emerge.
|
||||
@@ -0,0 +1,6 @@
|
||||
# proxmox_vm_placeholder module
|
||||
|
||||
Placeholder module directory for future shared Proxmox VM patterns.
|
||||
|
||||
This is intentionally empty until provider schemas, import ID formats,
|
||||
and non-destructive reconciliation strategy are validated.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Terraform Proxmox Inventory Layer
|
||||
|
||||
This directory codifies existing Proxmox infrastructure using an import-first reconciliation model.
|
||||
|
||||
## Purpose
|
||||
|
||||
- Track existing Proxmox VMs in Terraform.
|
||||
- Reconcile imported VM configuration into maintainable, explicit files.
|
||||
- Represent physical host metadata as structured Terraform locals/outputs.
|
||||
- Support documentation inventory and future downstream tooling.
|
||||
|
||||
## Current repository status
|
||||
|
||||
This directory already contains imported/reconciled VM resources (for example `docker`, `server-nixos`, `nix-cache`, `pbs`, `pihole`) plus host metadata locals/outputs.
|
||||
|
||||
This means it is no longer just a scaffold; treat it as active infrastructure inventory code.
|
||||
|
||||
## Workflow standard (brownfield)
|
||||
|
||||
1. Import one existing VM at a time.
|
||||
2. Confirm provider-specific import ID format.
|
||||
3. Inspect state/plan details.
|
||||
4. Keep hand-maintained `.tf` files focused and readable.
|
||||
5. Use `ignore_changes` only where drift noise is unavoidable.
|
||||
6. Stop when plan is sane/no-op for intended scope.
|
||||
|
||||
## File organization expectations
|
||||
|
||||
- Prefer one-resource-per-file patterns when practical.
|
||||
- Keep shared metadata in `locals`/outputs with clear descriptions.
|
||||
- Keep generated comments/config under ongoing cleanup rather than assuming generated output is final.
|
||||
|
||||
## Safety notes
|
||||
|
||||
- Do not run broad applies casually.
|
||||
- Do not commit real credentials or `.tfstate*`.
|
||||
- Keep changes incremental and reviewable.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [../README.md](../README.md)
|
||||
- [../../../docs/source-of-truth.md](../../../docs/source-of-truth.md)
|
||||
- [../../../docs/terraform-workflows.md](../../../docs/terraform-workflows.md)
|
||||
- [../../../docs/infrastructure-inventory.md](../../../docs/infrastructure-inventory.md)
|
||||
@@ -0,0 +1,76 @@
|
||||
# proxmox_virtual_environment_vm.docker:
|
||||
resource "proxmox_virtual_environment_vm" "docker" {
|
||||
name = "docker"
|
||||
node_name = "pve"
|
||||
scsi_hardware = "virtio-scsi-single"
|
||||
vm_id = 103
|
||||
|
||||
agent {
|
||||
enabled = true
|
||||
timeout = "15m"
|
||||
trim = false
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = 4
|
||||
numa = false
|
||||
sockets = 1
|
||||
type = "host"
|
||||
units = 1024
|
||||
}
|
||||
|
||||
disk {
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi0"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-103-disk-0"
|
||||
replicate = true
|
||||
size = 120
|
||||
ssd = false
|
||||
}
|
||||
disk {
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi1"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-103-disk-1"
|
||||
replicate = true
|
||||
size = 250
|
||||
ssd = false
|
||||
}
|
||||
|
||||
memory {
|
||||
dedicated = 8192
|
||||
floating = 4096
|
||||
keep_hugepages = false
|
||||
shared = 0
|
||||
}
|
||||
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
disconnected = false
|
||||
enabled = true
|
||||
firewall = true
|
||||
}
|
||||
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
vga,
|
||||
keyboard_layout,
|
||||
tablet_device,
|
||||
agent,
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
# Proxmox import-first workflow
|
||||
#
|
||||
# 1) Add one minimal resource block for one existing VM.
|
||||
# 2) Add an import block for that VM using the provider's required import ID format.
|
||||
# 3) Run:
|
||||
# terraform init
|
||||
# terraform plan
|
||||
# or:
|
||||
# ../scripts/reconcile_from_plan.sh -- -var-file=terraform.tfvars
|
||||
# 4) Review generated config carefully.
|
||||
# 5) Move only the useful arguments into a hand-maintained .tf file.
|
||||
# 6) Repeat until `terraform plan` is a no-op.
|
||||
|
||||
# IMPORTANT:
|
||||
# - Start with exactly ONE existing VM.
|
||||
# - Do not apply until plan is clean.
|
||||
# - Confirm the provider's exact import ID format before running import/plan.
|
||||
# - Do not import your whole environment at once.
|
||||
|
||||
# Example placeholder for one existing VM
|
||||
#resource "proxmox_virtual_environment_vm" "server-nixos" {
|
||||
# name = "server-nixos"
|
||||
# node_name = "pve"
|
||||
#}
|
||||
|
||||
# Example import block
|
||||
# REPLACE the id below with the exact import ID format required by your provider.
|
||||
# This is provider-specific and must be confirmed before use.
|
||||
#
|
||||
# Commonly this will involve the Proxmox node name and VM ID in some form.
|
||||
#
|
||||
import {
|
||||
to = proxmox_virtual_environment_vm.nix-cache
|
||||
id = "pve/105"
|
||||
}
|
||||
import {
|
||||
to = proxmox_virtual_environment_vm.server-nixos
|
||||
id = "pve/104"
|
||||
}
|
||||
import {
|
||||
to = proxmox_virtual_environment_vm.pihole
|
||||
id = "pve/108"
|
||||
}
|
||||
import {
|
||||
to = proxmox_virtual_environment_vm.pbs
|
||||
id = "pve/106"
|
||||
}
|
||||
import {
|
||||
to = proxmox_virtual_environment_vm.docker
|
||||
id = "pve/103"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# proxmox_virtual_environment_vm.nix-cache:
|
||||
resource "proxmox_virtual_environment_vm" "nix-cache" {
|
||||
name = "nix-cache"
|
||||
node_name = "pve"
|
||||
scsi_hardware = "virtio-scsi-single"
|
||||
vm_id = 105
|
||||
|
||||
agent {
|
||||
enabled = true
|
||||
timeout = "15m"
|
||||
trim = false
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = 2
|
||||
numa = false
|
||||
sockets = 1
|
||||
type = "x86-64-v2-AES"
|
||||
units = 1024
|
||||
}
|
||||
|
||||
disk {
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi0"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-105-disk-0"
|
||||
replicate = true
|
||||
size = 100
|
||||
ssd = false
|
||||
}
|
||||
|
||||
memory {
|
||||
dedicated = 2048
|
||||
floating = 0
|
||||
keep_hugepages = false
|
||||
shared = 0
|
||||
}
|
||||
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
disconnected = false
|
||||
enabled = true
|
||||
firewall = true
|
||||
}
|
||||
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
vga,
|
||||
keyboard_layout,
|
||||
tablet_device,
|
||||
agent,
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
output "proxmox_scaffold_ready" {
|
||||
description = "Indicates this directory is a placeholder scaffold for future Proxmox adoption."
|
||||
value = true
|
||||
}
|
||||
|
||||
output "proxmox_endpoint_configured" {
|
||||
description = "Whether a non-empty endpoint has been provided."
|
||||
value = var.proxmox_endpoint != ""
|
||||
}
|
||||
|
||||
output "physical_hosts" {
|
||||
description = "Physical host inventory used for documentation"
|
||||
value = local.physical_hosts
|
||||
}
|
||||
|
||||
output "virtual_hosts" {
|
||||
description = "Virtual host/VM inventory used for documentation"
|
||||
value = local.virtual_hosts
|
||||
}
|
||||
|
||||
|
||||
output "infrastructure_inventory" {
|
||||
description = "Combined infrastructure inventory"
|
||||
value = {
|
||||
physical_hosts = local.physical_hosts
|
||||
virtual_hosts = local.virtual_hosts
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# __generated__ by Terraform
|
||||
# Please review these resources and move them into your main configuration files.
|
||||
|
||||
# __generated__ by Terraform
|
||||
resource "proxmox_virtual_environment_vm" "pbs" {
|
||||
name = "pbs"
|
||||
node_name = "pve"
|
||||
scsi_hardware = "virtio-scsi-single"
|
||||
vm_id = 106
|
||||
agent {
|
||||
enabled = true
|
||||
timeout = "15m"
|
||||
trim = false
|
||||
}
|
||||
cpu {
|
||||
cores = 4
|
||||
numa = false
|
||||
sockets = 1
|
||||
type = "x86-64-v2-AES"
|
||||
units = 1024
|
||||
}
|
||||
disk {
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi0"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-106-disk-0"
|
||||
replicate = true
|
||||
size = 100
|
||||
ssd = false
|
||||
}
|
||||
disk {
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi1"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-106-disk-1"
|
||||
replicate = true
|
||||
size = 700
|
||||
ssd = false
|
||||
}
|
||||
memory {
|
||||
dedicated = 8192
|
||||
floating = 4096
|
||||
keep_hugepages = false
|
||||
shared = 0
|
||||
}
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
disconnected = false
|
||||
enabled = true
|
||||
firewall = true
|
||||
}
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
vga,
|
||||
keyboard_layout,
|
||||
tablet_device,
|
||||
agent,
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Pi-hole VM — DECOMMISSIONED
|
||||
# Pi-hole was decommissioned. DNS is now handled by FreeIPA (domain-controller.sweet.home).
|
||||
# DHCP is on the router. PXE DHCP is handled by the pxe-boot LXC.
|
||||
#
|
||||
# This resource block is kept as historical documentation of VMID 108.
|
||||
# If the VM has been destroyed on Proxmox, remove this file entirely.
|
||||
# If it still exists and needs to be removed via Terraform:
|
||||
# terraform destroy -target=proxmox_virtual_environment_vm.pihole
|
||||
|
||||
# resource "proxmox_virtual_environment_vm" "pihole" {
|
||||
# name = "pihole"
|
||||
# node_name = "pve"
|
||||
# vm_id = 108
|
||||
# }
|
||||
@@ -0,0 +1,11 @@
|
||||
provider "proxmox" {
|
||||
# Placeholder-only scaffold.
|
||||
# Confirm exact provider auth mode and endpoint details before real use.
|
||||
endpoint = var.proxmox_endpoint
|
||||
insecure = var.proxmox_insecure
|
||||
|
||||
# username = var.proxmox_username
|
||||
# password = var.proxmox_password
|
||||
|
||||
api_token = "${var.proxmox_api_token_id}=${var.proxmox_api_token_secret}"
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
locals {
|
||||
physical_hosts = {
|
||||
pve = {
|
||||
hostname = "pve"
|
||||
type = "physical"
|
||||
role = "proxmox"
|
||||
management_ip = "pve.sweet.home"
|
||||
os_family = "debian"
|
||||
hypervisor = "proxmox"
|
||||
location = "home"
|
||||
notes = "Primary Proxmox VE host"
|
||||
}
|
||||
raspberrypi = {
|
||||
hostname = "raspberrypi"
|
||||
type = "physical"
|
||||
role = "edge"
|
||||
management_ip = "raspberrypi.tail13f623.ts.net"
|
||||
os_family = "debian"
|
||||
hypervisor = null
|
||||
location = "riverglades"
|
||||
notes = "Raspberry Pi host"
|
||||
}
|
||||
}
|
||||
|
||||
# Virtual host inventory for documentation output. This is intentionally
|
||||
# concise and shaped for docs tooling (not a full provider object dump).
|
||||
virtual_hosts = {
|
||||
docker = {
|
||||
name = "docker"
|
||||
type = "virtual"
|
||||
role = "docker-host"
|
||||
proxmox_node = "pve"
|
||||
vm_id = 103
|
||||
management_ip = ""
|
||||
os_family = "linux"
|
||||
notes = "Primary Docker VM"
|
||||
}
|
||||
server_nixos = {
|
||||
name = "server-nixos"
|
||||
type = "virtual"
|
||||
role = "nixos-server"
|
||||
proxmox_node = "pve"
|
||||
vm_id = 104
|
||||
management_ip = ""
|
||||
os_family = "nixos"
|
||||
notes = "General-purpose NixOS VM"
|
||||
}
|
||||
nix_cache = {
|
||||
name = "nix-cache"
|
||||
type = "virtual"
|
||||
role = "cache"
|
||||
proxmox_node = "pve"
|
||||
vm_id = 105
|
||||
management_ip = ""
|
||||
os_family = "linux"
|
||||
notes = "Nix binary cache VM"
|
||||
}
|
||||
pbs = {
|
||||
name = "pbs"
|
||||
type = "virtual"
|
||||
role = "backup"
|
||||
proxmox_node = "pve"
|
||||
vm_id = 106
|
||||
management_ip = ""
|
||||
os_family = "linux"
|
||||
notes = "Proxmox Backup Server VM"
|
||||
}
|
||||
pihole = {
|
||||
name = "pihole"
|
||||
type = "virtual"
|
||||
role = "dns"
|
||||
proxmox_node = "pve"
|
||||
vm_id = 108
|
||||
management_ip = ""
|
||||
os_family = "linux"
|
||||
notes = "DNS filtering VM"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
# __generated__ by Terraform
|
||||
# Please review these resources and move them into your main configuration files.
|
||||
|
||||
# __generated__ by Terraform
|
||||
resource "proxmox_virtual_environment_vm" "server-nixos" {
|
||||
name = "server-nixos"
|
||||
node_name = "pve"
|
||||
scsi_hardware = "virtio-scsi-single"
|
||||
vm_id = 104
|
||||
agent {
|
||||
enabled = true
|
||||
timeout = "15m"
|
||||
trim = false
|
||||
}
|
||||
cpu {
|
||||
cores = 4
|
||||
numa = false
|
||||
sockets = 1
|
||||
type = "x86-64-v2-AES"
|
||||
units = 1024
|
||||
}
|
||||
disk {
|
||||
aio = "io_uring"
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi0"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-104-disk-0"
|
||||
replicate = true
|
||||
size = 32
|
||||
ssd = false
|
||||
}
|
||||
disk {
|
||||
aio = "io_uring"
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi1"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-104-disk-1"
|
||||
replicate = true
|
||||
size = 200
|
||||
ssd = false
|
||||
}
|
||||
disk {
|
||||
aio = "io_uring"
|
||||
backup = true
|
||||
cache = "none"
|
||||
datastore_id = "local-lvm"
|
||||
discard = "ignore"
|
||||
file_format = "raw"
|
||||
interface = "scsi2"
|
||||
iothread = false
|
||||
path_in_datastore = "vm-104-disk-2"
|
||||
replicate = true
|
||||
size = 200
|
||||
ssd = false
|
||||
}
|
||||
memory {
|
||||
dedicated = 4096
|
||||
floating = 2048
|
||||
keep_hugepages = false
|
||||
shared = 0
|
||||
}
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
disconnected = false
|
||||
enabled = true
|
||||
firewall = true
|
||||
}
|
||||
operating_system {
|
||||
type = "l26"
|
||||
}
|
||||
lifecycle {
|
||||
ignore_changes = [
|
||||
vga,
|
||||
keyboard_layout,
|
||||
tablet_device,
|
||||
agent,
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Example placeholders only. Do not commit real credentials.
|
||||
|
||||
proxmox_endpoint = "https://pve.example.local:8006/api2/json"
|
||||
proxmox_insecure = false
|
||||
|
||||
# Use either username/password or API token based on your chosen auth flow.
|
||||
proxmox_username = "root@pam"
|
||||
proxmox_password = "REPLACE_ME"
|
||||
proxmox_api_token = "REPLACE_ME"
|
||||
@@ -0,0 +1,37 @@
|
||||
variable "proxmox_endpoint" {
|
||||
description = "Proxmox API endpoint URL, for example https://pve.example.local:8006/api2/json"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "proxmox_insecure" {
|
||||
description = "Set true only for local testing with self-signed TLS; prefer false in stable environments."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
#variable "proxmox_username" {
|
||||
# description = "Username for password-based auth (placeholder; optional if token auth is used)."
|
||||
# type = string
|
||||
# default = ""
|
||||
#}
|
||||
|
||||
#variable "proxmox_password" {
|
||||
# description = "Password for password-based auth (placeholder; optional if token auth is used)."
|
||||
# type = string
|
||||
# default = ""
|
||||
# sensitive = true
|
||||
#}
|
||||
|
||||
variable "proxmox_api_token_id" {
|
||||
type = string
|
||||
description = "Proxmox API token ID, e.g. terraform@pve!tf"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "proxmox_api_token_secret" {
|
||||
description = "API token for token-based auth (placeholder; optional if username/password is used)."
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6.0"
|
||||
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = "0.68.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
reconcile_from_plan.sh [--output-file <path>] [--] [terraform plan args...]
|
||||
|
||||
Description:
|
||||
Runs `terraform plan` with `-generate-config-out` and writes the generated
|
||||
configuration into a tracked Terraform file (default:
|
||||
`zz_generated_from_plan.auto.tf`).
|
||||
|
||||
This is designed for import-first workflows where `import { ... }` blocks are
|
||||
present and Terraform can generate missing resource arguments from live
|
||||
infrastructure.
|
||||
|
||||
Options:
|
||||
--output-file <path> Destination .tf/.auto.tf file to receive generated
|
||||
configuration. Default: zz_generated_from_plan.auto.tf
|
||||
-h, --help Show this help text.
|
||||
|
||||
Examples:
|
||||
./reconcile_from_plan.sh
|
||||
./reconcile_from_plan.sh --output-file generated_imports.auto.tf -- -var-file=terraform.tfvars
|
||||
USAGE
|
||||
}
|
||||
|
||||
output_file="zz_generated_from_plan.auto.tf"
|
||||
plan_args=()
|
||||
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--output-file)
|
||||
if (($# < 2)); then
|
||||
echo "error: --output-file requires a value" >&2
|
||||
exit 1
|
||||
fi
|
||||
output_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
plan_args=("$@")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
plan_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v terraform >/dev/null 2>&1; then
|
||||
echo "error: terraform is not installed or not in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "main.tf" ]] && ! compgen -G "*.tf" >/dev/null; then
|
||||
echo "error: no Terraform configuration (*.tf) found in $(pwd)" >&2
|
||||
echo "run this script from a Terraform module directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
generated_tmp_dir="$(mktemp -d -t terraform-generated-XXXXXX)"
|
||||
generated_tmp="$generated_tmp_dir/generated.tf"
|
||||
# terraform plan -generate-config-out requires a path that does not already exist
|
||||
trap 'rm -rf "$generated_tmp_dir"' EXIT
|
||||
|
||||
echo "Running: terraform plan -generate-config-out=$generated_tmp ${plan_args[*]-}"
|
||||
set +e
|
||||
terraform plan -generate-config-out="$generated_tmp" "${plan_args[@]}"
|
||||
plan_exit=$?
|
||||
set -e
|
||||
|
||||
if [[ $plan_exit -ne 0 && $plan_exit -ne 2 ]]; then
|
||||
echo "error: terraform plan failed with exit code $plan_exit" >&2
|
||||
exit "$plan_exit"
|
||||
fi
|
||||
|
||||
if [[ ! -s "$generated_tmp" ]]; then
|
||||
echo "No generated configuration was produced."
|
||||
echo "Tip: ensure you have import blocks and resources eligible for config generation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat > "$output_file" <<EOF2
|
||||
# -----------------------------------------------------------------------------
|
||||
# AUTO-GENERATED BY reconcile_from_plan.sh
|
||||
# Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
# Source: terraform plan -generate-config-out
|
||||
# Review carefully before apply.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
EOF2
|
||||
cat "$generated_tmp" >> "$output_file"
|
||||
|
||||
terraform fmt "$output_file" >/dev/null
|
||||
|
||||
echo "Generated configuration written to: $output_file"
|
||||
echo "Next step: review this file and run terraform plan again to confirm intent."
|
||||
Reference in New Issue
Block a user