Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd35f5f21b | ||
|
|
d208c17509 | ||
|
|
853928b6b1 | ||
|
|
3fac750430 |
@@ -1,122 +0,0 @@
|
||||
# TEMPORARY shims v3 — rustfs LXCs 800/801/802, decommission in progress.
|
||||
#
|
||||
# Stage 2 (#270) failed: both destroy attempts died instantly with
|
||||
# "context deadline exceeded" on the pre-destroy status GET, while the PVE
|
||||
# API answers the same call in <1s. Cause: the bpg provider reads
|
||||
# `timeout_delete` from STATE at destroy time, and importing a resource
|
||||
# whose config declares no timeouts stored 0 — a zero-second context.
|
||||
# (The import/plan read path uses its own timeout, which is why stage 1
|
||||
# planned clean.)
|
||||
#
|
||||
# Fix: declare the timeouts explicitly, apply ONCE so they land in state
|
||||
# (in-place no-API update), then delete this file again and the next apply
|
||||
# destroys the containers.
|
||||
#
|
||||
# v3: v2 declared ONLY the ids, `started`, `unprivileged` and the timeouts,
|
||||
# claiming "the other attributes mirror the real containers so the plan stays
|
||||
# in-place". They did not. Everything absent from config planned as a removal,
|
||||
# and two of those removals — `mount_point` and `operating_system` — force
|
||||
# replacement, so the plan came back `must be replaced` for all three. Applying
|
||||
# that would have destroyed and recreated containers tagged `critical`,
|
||||
# `state-backend`, `terraform-backend` instead of nudging a timeout into state.
|
||||
#
|
||||
# So the blocks below are not decoration: each one exists to keep an attribute
|
||||
# out of the diff. They mirror the live containers, read from the plan's own
|
||||
# state values. `tags` is declared for the same reason — without it the apply
|
||||
# strips the tags off the real containers.
|
||||
#
|
||||
# 1. merge; run PIPELINE=deploy-infrastructure (plan MUST show in-place
|
||||
# update only, no replace), then TOFU_APPLY=true
|
||||
# 2. follow-up PR deletes this file; plan shows 3 destroys; apply.
|
||||
|
||||
locals {
|
||||
# Per-container values mirrored from live state. hostname and mac_address
|
||||
# differ per container; declaring them wrong would itself force a diff.
|
||||
rustfs_decommission = {
|
||||
"800" = { node = "saturn", hostname = "rustfs-1.srv.renner.uno", mac = "BC:24:11:0B:E5:7B" }
|
||||
"801" = { node = "jupiter", hostname = "rustfs-2.srv.renner.uno", mac = "BC:24:11:EA:1D:38" }
|
||||
"802" = { node = "neptune", hostname = "rustfs-3.srv.renner.uno", mac = "BC:24:11:8F:F4:67" }
|
||||
}
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_container" "rustfs_decommission" {
|
||||
for_each = local.rustfs_decommission
|
||||
|
||||
node_name = each.value.node
|
||||
vm_id = tonumber(each.key)
|
||||
started = false
|
||||
unprivileged = true
|
||||
start_on_boot = true
|
||||
|
||||
tags = [
|
||||
"critical",
|
||||
"rustfs",
|
||||
"state-backend",
|
||||
"terraform-backend",
|
||||
]
|
||||
|
||||
timeout_create = 1800
|
||||
timeout_clone = 1800
|
||||
timeout_update = 1800
|
||||
timeout_delete = 1800
|
||||
timeout_start = 300
|
||||
|
||||
cpu {
|
||||
architecture = "amd64"
|
||||
cores = 1
|
||||
units = 1024
|
||||
}
|
||||
|
||||
memory {
|
||||
dedicated = 1024
|
||||
swap = 0
|
||||
}
|
||||
|
||||
disk {
|
||||
datastore_id = "local"
|
||||
size = 10
|
||||
}
|
||||
|
||||
# forces replacement if absent — this is the block that broke v2.
|
||||
mount_point {
|
||||
path = "/mnt/data"
|
||||
size = "50G"
|
||||
volume = "local:${each.key}/vm-${each.key}-disk-1.raw"
|
||||
backup = true
|
||||
replicate = true
|
||||
}
|
||||
|
||||
network_interface {
|
||||
name = "eth0"
|
||||
bridge = "v40srv"
|
||||
enabled = true
|
||||
firewall = false
|
||||
mac_address = each.value.mac
|
||||
}
|
||||
|
||||
initialization {
|
||||
hostname = each.value.hostname
|
||||
|
||||
ip_config {
|
||||
ipv4 {
|
||||
address = "dhcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# operating_system is NOT declared, and must not be. The provider requires
|
||||
# template_file_id inside it, which is a create-time-only value: PVE does not
|
||||
# report the original template for an existing container, so it is absent
|
||||
# from state and cannot be mirrored. Declaring the block without it fails
|
||||
# validation ("Missing required argument"); declaring it with a guessed
|
||||
# template would be a lie that only surfaces if something ever recreates
|
||||
# these.
|
||||
#
|
||||
# Left undeclared it would plan as a removal, and removing operating_system
|
||||
# forces replacement — the exact failure this file exists to avoid. So it is
|
||||
# ignored instead. The timeouts above are deliberately NOT ignored: they are
|
||||
# the one thing this shim needs to actually apply.
|
||||
lifecycle {
|
||||
ignore_changes = [operating_system]
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -59,8 +59,25 @@ provider "unifi" {
|
||||
|
||||
# UniFi Network v2 API (policy-table NAT rules). Same controller and API key
|
||||
# as the unifi provider above; the v2 endpoints are simply not covered by it.
|
||||
# UniFi PUT quirk: updates may require _id in the body, which restapi does
|
||||
# not send — if an in-place update 400s, taint the object so CI recreates it.
|
||||
# UniFi PUT quirk — DISPROVEN 2026-08-19, left here because it was wrong for
|
||||
# long enough to do damage. The claim was that a UniFi PUT may require _id in
|
||||
# the body, which restapi does not send, and that a 400 should be answered by
|
||||
# tainting the object so CI recreates it. Tested directly against a live rule
|
||||
# with exactly the body tofu sends:
|
||||
#
|
||||
# PUT /proxy/network/v2/api/site/default/nat/6a61c7d7bbd7b2ca90981f51
|
||||
# body: the object, _id removed -> HTTP 200, object returned intact
|
||||
# rule re-read and diffed before/after -> byte-identical
|
||||
# rule count -> 60 before, 60 after
|
||||
#
|
||||
# Confirmed again at scale by apply 5675: all 60 rules "Modifications complete
|
||||
# after 0s", no 400s, ci.renner.uno and immich.renner.uno both still 200.
|
||||
#
|
||||
# This mattered. A warning that applying might break firewall rules is a
|
||||
# warning nobody argues with, so the apply did not get run — and the
|
||||
# renner-uno-k8s-mariadb bucket added in b976a67b sat uncreated for three days
|
||||
# while the WordPress backup failed nightly with "The specified bucket does not
|
||||
# exist". Applying these NAT rules is safe. Do not taint them.
|
||||
#
|
||||
# create_returns_object, NOT write_returns_object. Both make the provider store
|
||||
# the API response into `data`; write_returns_object does it for PUT as well as
|
||||
|
||||
@@ -107,17 +107,51 @@ resource "restapi_object" "hairpin" {
|
||||
for_each = local.hairpin_rules
|
||||
|
||||
path = "/nat"
|
||||
|
||||
# KNOWN PERMANENT DIFF — cosmetic. Do not try to fix it with a provider
|
||||
# setting; four have been tried and none work. Recorded here so the next
|
||||
# person does not spend a day rediscovering that.
|
||||
#
|
||||
# Every plan shows all 60 of these as "will be updated in-place" with the
|
||||
# entire diff being one line:
|
||||
#
|
||||
# ~ data = jsonencode( ~ { - _id = "6a61c7d7bbd7b2ca90981f51"
|
||||
# # (15 unchanged attributes hidden) } )
|
||||
#
|
||||
# The UDM assigns _id and returns it; the provider puts the response into
|
||||
# `data`; the `data` below never sets it. So state carries a field the config
|
||||
# does not. It survives an apply: 5675 rewrote all 60, and the very next plan
|
||||
# (5677) showed the same 60 again.
|
||||
#
|
||||
# Tried, with the plan that disproved each — do not repeat these:
|
||||
#
|
||||
# ignore_changes_to = ["_id"] 5661 60 remained
|
||||
# ignore_all_server_changes = true 5662 60 remained
|
||||
# restapi 3.x + ignore_server_additions = true 5667 60 remained, and
|
||||
# added an api_data diff
|
||||
# write_returns_object -> create_returns_object 5677 60 remained
|
||||
#
|
||||
# The first three cannot work in principle: `data` is a configured attribute,
|
||||
# so Terraform core diffs config against state before any provider-side ignore
|
||||
# logic runs. The fourth was the better theory — that PUT responses were
|
||||
# re-polluting `data` — and it is wrong too, which means the response is
|
||||
# written into `data` on the READ path. Settling it properly needs the
|
||||
# provider source, not more guesses at the schema.
|
||||
#
|
||||
# It is safe to apply through. Verified twice: a single PUT without _id
|
||||
# returns 200 and leaves the rule byte-identical (see providers.tf), and
|
||||
# apply 5675 modified all 60 with no errors and no service impact.
|
||||
data = jsonencode({
|
||||
description = "Hairpin ${each.key}"
|
||||
type = "DNAT"
|
||||
enabled = true
|
||||
exclude = false
|
||||
in_interface = each.value.network_id
|
||||
ip_address = each.value.endpoint.target
|
||||
ip_version = each.value.endpoint.ip_version
|
||||
protocol = "tcp_udp"
|
||||
rule_index = each.value.rule_index
|
||||
logging = false
|
||||
description = "Hairpin ${each.key}"
|
||||
type = "DNAT"
|
||||
enabled = true
|
||||
exclude = false
|
||||
in_interface = each.value.network_id
|
||||
ip_address = each.value.endpoint.target
|
||||
ip_version = each.value.endpoint.ip_version
|
||||
protocol = "tcp_udp"
|
||||
rule_index = each.value.rule_index
|
||||
logging = false
|
||||
is_predefined = false
|
||||
pppoe_use_base_interface = false
|
||||
setting_preference = "manual"
|
||||
|
||||
Reference in New Issue
Block a user