From 23d1e0ef2c88ad028029aac6e678272ecbf84121 Mon Sep 17 00:00:00 2001 From: jenkins Date: Thu, 16 Jul 2026 03:25:30 -0300 Subject: [PATCH] terraform: add atlas day-zero scaffold --- .gitignore | 11 ++ README.md | 2 + ci/tests/glue/conftest.py | 34 ++++ terraform/atlas/.terraform.lock.hcl | 43 +++++ terraform/atlas/README.md | 97 ++++++++++ terraform/atlas/main.tf | 22 +++ terraform/atlas/outputs.tf | 29 +++ terraform/atlas/providers.tf | 4 + terraform/atlas/terraform.tfvars.example | 173 ++++++++++++++++++ terraform/atlas/variables.tf | 93 ++++++++++ terraform/atlas/versions.tf | 18 ++ .../modules/atlas_inventory_labels/main.tf | 74 ++++++++ .../modules/atlas_inventory_labels/outputs.tf | 27 +++ .../atlas_inventory_labels/variables.tf | 49 +++++ .../modules/flux_day_zero_render/main.tf | 38 ++++ .../modules/flux_day_zero_render/outputs.tf | 3 + .../modules/flux_day_zero_render/variables.tf | 26 +++ 17 files changed, 743 insertions(+) create mode 100644 ci/tests/glue/conftest.py create mode 100644 terraform/atlas/.terraform.lock.hcl create mode 100644 terraform/atlas/README.md create mode 100644 terraform/atlas/main.tf create mode 100644 terraform/atlas/outputs.tf create mode 100644 terraform/atlas/providers.tf create mode 100644 terraform/atlas/terraform.tfvars.example create mode 100644 terraform/atlas/variables.tf create mode 100644 terraform/atlas/versions.tf create mode 100644 terraform/modules/atlas_inventory_labels/main.tf create mode 100644 terraform/modules/atlas_inventory_labels/outputs.tf create mode 100644 terraform/modules/atlas_inventory_labels/variables.tf create mode 100644 terraform/modules/flux_day_zero_render/main.tf create mode 100644 terraform/modules/flux_day_zero_render/outputs.tf create mode 100644 terraform/modules/flux_day_zero_render/variables.tf diff --git a/.gitignore b/.gitignore index 7543bbfb0..136020aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,17 @@ __pycache__/ *.py[cod] .pytest_cache +.coverage +build/ +test-results/ +artifacts/ .venv .venv-ci tmp/ +.terraform/ +**/.terraform/ +*.tfvars +*.tfstate +*.tfstate.* +crash.log +terraform/atlas/generated/ diff --git a/README.md b/README.md index c99975c7d..fb8a0e08b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ This repo contains cluster configuration consumed by Flux: This repo is **not** the Ananke application source repo. Ananke lives in `bstein/ananke` and orchestrates host-side shutdown/startup behavior around this desired state. +Terraform lives under `terraform/atlas` for declarative day-zero inventory, Kubernetes Node labels, and generated Ananke inventory input. It must not take ownership of Flux-managed workload manifests. + ## Validation workflow ```bash diff --git a/ci/tests/glue/conftest.py b/ci/tests/glue/conftest.py new file mode 100644 index 000000000..c87c5905b --- /dev/null +++ b/ci/tests/glue/conftest.py @@ -0,0 +1,34 @@ +"""Skip live glue checks when the VictoriaMetrics endpoint is unavailable.""" + +from __future__ import annotations + +import os +import socket +from urllib.parse import urlparse + +import pytest + + +def _endpoint_available() -> bool: + """Return whether the configured VictoriaMetrics host resolves locally.""" + vm_url = os.environ.get("VM_URL", "http://victoria-metrics-single-server:8428").rstrip("/") + parsed = urlparse(vm_url) + host = parsed.hostname + port = parsed.port or (443 if parsed.scheme == "https" else 80) + if not host: + return False + try: + socket.getaddrinfo(host, port) + except socket.gaierror: + return False + return True + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: + """Mark glue tests skipped when the VictoriaMetrics host cannot resolve.""" + del config + if _endpoint_available(): + return + skip_marker = pytest.mark.skip(reason="VictoriaMetrics endpoint is not reachable in this workspace") + for item in items: + item.add_marker(skip_marker) diff --git a/terraform/atlas/.terraform.lock.hcl b/terraform/atlas/.terraform.lock.hcl new file mode 100644 index 000000000..0f5d56651 --- /dev/null +++ b/terraform/atlas/.terraform.lock.hcl @@ -0,0 +1,43 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.2.1" + constraints = "~> 3.2" + hashes = [ + "h1:XccFuTe/eJ94vkoZNYUL3wsdXJwlxHguIY36jildRQ8=", + "zh:067fe16a852d42e0f571712e36cb3e71855f917ea2041415e155f56ebc480d7f", + "zh:2815e174f8f0f032ea3a64f2196740ad000a39f88ae5646e7061bf15ed589f62", + "zh:2f94f6b689c59c43e596e724228f2861095d02c2a2ac2a257a4619667135ac75", + "zh:3e807310c84f11561b9ba06b978f03c46cfeca2e84ad0803d34d5d30a8a637cb", + "zh:5cba6f92202c60cac6898141356420709f5341b80ae4c360725cc647f86188ff", + "zh:72b841b6f0820d8f87c3d7c5a3611c35121ab9a4c1db4ea7a98b0319f209e474", + "zh:74770b892ee9b04829d92318d9e8ca96f8143b0c6c766e4141901908173fd01d", + "zh:7a723c8ebf9e218d0f7a0cfe6c0437f2b5eeb7ae015a14fad16e0f7fd9ef79ab", + "zh:a0f5073b2636a3894d4e9dd1b6853d5f324dd78728313bff79b842e5e9eca96f", + "zh:c13241cba993ef63a537beb6a1caf00e233bb045b50d197349530de0ee3276d5", + "zh:d52826f4b0227b7db99ea4a1d48f49a0bfb440563c92ebd2f8faec273c856c2d", + "zh:dc1cf5505a39a264a650b0830f74150ad02368787e5ead89e4007034f8f47831", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.9.0" + constraints = "~> 2.5" + hashes = [ + "h1:9rBZCMNpxKwMlRbWH2QpwD3kqUCAejdOZQ/aiiDObXQ=", + "zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0", + "zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64", + "zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90", + "zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c", + "zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8", + "zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8", + "zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b", + "zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9", + "zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195", + "zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3", + "zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18", + ] +} diff --git a/terraform/atlas/README.md b/terraform/atlas/README.md new file mode 100644 index 000000000..9a5c91653 --- /dev/null +++ b/terraform/atlas/README.md @@ -0,0 +1,97 @@ +# Atlas Terraform + +Terraform owns the declarative day-zero pieces that are not Flux workload state: + +- Atlas host/node inventory used to generate Ananke config input. +- Required Kubernetes Node labels through `kubernetes_labels`. +- Optional render-only Flux day-zero manifests from `clusters/atlas/flux-system`. + +Terraform does not own Flux-managed workload manifests. Do not move `services/`, `infrastructure/`, or Flux `Kustomization` trees under Terraform. Do not make manual `kubectl` edits for durable Kubernetes state. + +## Ownership + +Terraform owns: + +- node names, IPs, roles, managed-node membership, ignored/unavailable markers +- default SSH user, port, config path, identity path emitted into Ananke inventory +- `startup.required_node_labels` values, applied as Kubernetes Node labels +- generated `ananke.inventory.yaml` + +Flux owns: + +- workloads, platform services, Helm releases, Flux `Kustomization` resources, and service manifests +- steady-state reconciliation of objects under `clusters/atlas/flux-system`, `infrastructure/`, and `services/` + +Ananke owns: + +- UPS shutdown decisions +- SSH repair and host recovery +- k3s restart/reboot +- Longhorn/runtime recovery +- cordon/uncordon, pod recycling, Flux suspend/resume during recovery +- startup validation/reporting for Terraform-owned labels when `required_node_labels_mode: validate` + +## Run + +```bash +cd terraform/atlas +cp terraform.tfvars.example terraform.tfvars +terraform init +terraform plan +``` + +For local syntax/provider validation that does not touch the cluster: + +```bash +terraform init -backend=false +terraform validate +``` + +Only run `terraform apply` after reviewing the plan and confirming label ownership. The Kubernetes provider uses `var.kubeconfig_path`, defaulting to `~/.kube/config`; no kubeconfig or secret material belongs in this repo. + +The backend is local by default. When remote state is ready, migrate state with `terraform init -migrate-state` after adding the remote backend configuration in a separate reviewed change. + +## Generated Ananke Fragment + +The inventory module writes `generated/ananke.inventory.yaml` by default. It contains no secrets and is intended to be installed beside the main Ananke config: + +```bash +sudo ANANKE_INVENTORY_FRAGMENT=/opt/titan-iac/terraform/atlas/generated/ananke.inventory.yaml /opt/ananke/scripts/install.sh +``` + +Ananke auto-loads `/etc/ananke/ananke.inventory.yaml` when present. The generated fragment sets: + +- `ssh_node_hosts` +- `ssh_managed_nodes` +- `control_planes` +- `workers` +- `startup.required_node_labels` +- `startup.required_node_labels_mode: validate` +- `startup.ignore_unavailable_nodes` + +## Node Labels + +This tree uses the HashiCorp Kubernetes provider `kubernetes_labels` resource. That resource manages only labels declared in Terraform and ignores existing labels not declared here. Leave `force_node_label_conflicts = false` for normal plans. Set it true only during a deliberate adoption when field ownership conflicts are understood. + +## Flux Day-Zero + +`clusters/atlas/flux-system/gotk-components.yaml` and `gotk-sync.yaml` already exist in the Flux bootstrap tree, so Terraform does not manage those objects by default. + +The `flux_day_zero` module can render those artifacts into one file when `render_flux_day_zero_manifests = true`. Keep `apply_flux_day_zero_manifests = false` for steady state. Enabling apply runs `kubectl apply --server-side` and is only for an empty-cluster bootstrap with explicit operator intent. + +## Migration Plan + +1. Copy `terraform.tfvars.example` to `terraform.tfvars` and adjust only local paths. +2. Run `terraform init -backend=false` and `terraform validate`. +3. Run `terraform plan` in plan-only mode and review Node label changes. +4. Adopt existing label state with `kubernetes_labels` resources. This resource does not use `terraform import`; creating it against existing Nodes is the adoption step. +5. Install the generated Ananke fragment and confirm Ananke reads `required_node_labels_mode: validate`. +6. Run `terraform apply` for labels after the plan is clean. +7. Run Ananke startup dry-run/status and confirm it reports label drift instead of applying labels. +8. Enable in production after Ananke is validating only and Terraform is the only steady-state writer for these labels. + +## Conflict Rules + +- Do not add Terraform resources for any object already in Flux unless it is a one-time day-zero bootstrap action and Terraform will not keep steady-state ownership. +- Do not add Flux manifests for Node labels Terraform manages here. +- Do not use Terraform for recovery behavior that Ananke performs imperatively. diff --git a/terraform/atlas/main.tf b/terraform/atlas/main.tf new file mode 100644 index 000000000..150f336f6 --- /dev/null +++ b/terraform/atlas/main.tf @@ -0,0 +1,22 @@ +module "inventory_labels" { + source = "../modules/atlas_inventory_labels" + + nodes = var.nodes + default_ssh_user = var.default_ssh_user + default_ssh_port = var.default_ssh_port + default_ssh_config_file = var.default_ssh_config_file + default_ssh_identity_file = var.default_ssh_identity_file + ananke_inventory_fragment_path = var.ananke_inventory_fragment_path + label_field_manager = var.node_label_field_manager + force_node_label_conflicts = var.force_node_label_conflicts +} + +module "flux_day_zero" { + source = "../modules/flux_day_zero_render" + + enabled = var.render_flux_day_zero_manifests + apply_manifests = var.apply_flux_day_zero_manifests + kubeconfig_path = var.kubeconfig_path + flux_system_path = abspath("${path.module}/../../clusters/atlas/flux-system") + output_path = var.flux_day_zero_output_path +} diff --git a/terraform/atlas/outputs.tf b/terraform/atlas/outputs.tf new file mode 100644 index 000000000..b052a52f2 --- /dev/null +++ b/terraform/atlas/outputs.tf @@ -0,0 +1,29 @@ +output "control_planes" { + value = module.inventory_labels.control_planes + description = "Terraform-derived Ananke control plane inventory." +} + +output "workers" { + value = module.inventory_labels.workers + description = "Terraform-derived Ananke worker inventory." +} + +output "ssh_managed_nodes" { + value = module.inventory_labels.ssh_managed_nodes + description = "Terraform-derived Ananke SSH managed node list." +} + +output "required_node_labels" { + value = module.inventory_labels.required_node_labels + description = "Node labels Terraform manages through kubernetes_labels." +} + +output "ananke_inventory_fragment_path" { + value = module.inventory_labels.ananke_inventory_fragment_path + description = "Generated Ananke inventory fragment path." +} + +output "flux_day_zero_manifest_path" { + value = module.flux_day_zero.rendered_manifest_path + description = "Rendered day-zero Flux manifest path when enabled." +} diff --git a/terraform/atlas/providers.tf b/terraform/atlas/providers.tf new file mode 100644 index 000000000..3cc9c261e --- /dev/null +++ b/terraform/atlas/providers.tf @@ -0,0 +1,4 @@ +provider "kubernetes" { + config_path = pathexpand(var.kubeconfig_path) + config_context = var.kubeconfig_context +} diff --git a/terraform/atlas/terraform.tfvars.example b/terraform/atlas/terraform.tfvars.example new file mode 100644 index 000000000..b7f57bd69 --- /dev/null +++ b/terraform/atlas/terraform.tfvars.example @@ -0,0 +1,173 @@ +kubeconfig_path = "~/.kube/config" + +default_ssh_user = "atlas" +default_ssh_port = 2277 +default_ssh_config_file = "/home/atlas/.ssh/config" +default_ssh_identity_file = "/home/atlas/.ssh/id_ed25519" + +ananke_inventory_fragment_path = "generated/ananke.inventory.yaml" +node_label_field_manager = "terraform-atlas-node-labels" +force_node_label_conflicts = false + +render_flux_day_zero_manifests = false +apply_flux_day_zero_manifests = false + +nodes = { + titan-db = { + ip = "192.168.22.10" + role = "recovery" + } + titan-0a = { + ip = "192.168.22.11" + role = "control-plane" + } + titan-0b = { + ip = "192.168.22.12" + role = "control-plane" + } + titan-0c = { + ip = "192.168.22.13" + role = "control-plane" + } + titan-04 = { + ip = "192.168.22.30" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-05 = { + ip = "192.168.22.31" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-06 = { + ip = "192.168.22.32" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-07 = { + ip = "192.168.22.33" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-08 = { + ip = "192.168.22.34" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-09 = { + ip = "192.168.22.35" + role = "worker" + ignore_unavailable = true + labels = { + "node-role.kubernetes.io/worker" = "true" + "ananke.bstein.dev/harbor-bootstrap" = "true" + } + } + titan-10 = { + ip = "192.168.22.36" + role = "worker" + ignore_unavailable = true + } + titan-11 = { + ip = "192.168.22.37" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-12 = { + ip = "192.168.22.40" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-13 = { + ip = "192.168.22.41" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-14 = { + ip = "192.168.22.42" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-15 = { + ip = "192.168.22.43" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-17 = { + ip = "192.168.22.45" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-18 = { + ip = "192.168.22.46" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-19 = { + ip = "192.168.22.47" + role = "worker" + labels = { + "node-role.kubernetes.io/worker" = "true" + "longhorn-host" = "true" + } + } + titan-20 = { + ip = "192.168.22.20" + role = "worker" + } + titan-21 = { + ip = "192.168.22.21" + role = "worker" + } + titan-22 = { + ip = "192.168.22.22" + role = "worker" + } + titan-23 = { + ip = "192.168.22.24" + role = "worker" + labels = { + "longhorn-host" = "true" + } + } + titan-24 = { + ip = "192.168.22.26" + role = "worker" + } +} diff --git a/terraform/atlas/variables.tf b/terraform/atlas/variables.tf new file mode 100644 index 000000000..3d124f4ec --- /dev/null +++ b/terraform/atlas/variables.tf @@ -0,0 +1,93 @@ +variable "kubeconfig_path" { + type = string + description = "Kubeconfig used by the Kubernetes provider for Atlas day-zero/declarative resources." + default = "~/.kube/config" +} + +variable "kubeconfig_context" { + type = string + description = "Optional kubeconfig context. Leave null to use the kubeconfig current-context." + default = null +} + +variable "default_ssh_user" { + type = string + description = "Default SSH user emitted into the generated Ananke inventory fragment." + default = "atlas" +} + +variable "default_ssh_port" { + type = number + description = "Default SSH port emitted into the generated Ananke inventory fragment." + default = 2277 +} + +variable "default_ssh_config_file" { + type = string + description = "Default SSH config file emitted into the generated Ananke inventory fragment." + default = "/home/atlas/.ssh/config" +} + +variable "default_ssh_identity_file" { + type = string + description = "Default SSH identity file emitted into the generated Ananke inventory fragment." + default = "/home/atlas/.ssh/id_ed25519" +} + +variable "ananke_inventory_fragment_path" { + type = string + description = "Where Terraform writes the generated non-secret Ananke inventory fragment." + default = "generated/ananke.inventory.yaml" +} + +variable "node_label_field_manager" { + type = string + description = "Kubernetes server-side apply field manager for Terraform-owned node labels." + default = "terraform-atlas-node-labels" +} + +variable "force_node_label_conflicts" { + type = bool + description = "Set true only during a deliberate node-label adoption if Kubernetes field ownership conflicts." + default = false +} + +variable "nodes" { + type = map(object({ + ip = string + role = string + managed = optional(bool, true) + ignore_unavailable = optional(bool, false) + ssh_user = optional(string) + ssh_port = optional(number) + ssh_config_file = optional(string) + ssh_identity_file = optional(string) + labels = optional(map(string), {}) + })) + description = "Atlas host and Kubernetes node inventory. role is control-plane, worker, or recovery." + + validation { + condition = alltrue([ + for node in values(var.nodes) : contains(["control-plane", "worker", "recovery"], node.role) + ]) + error_message = "Each node role must be one of: control-plane, worker, recovery." + } +} + +variable "render_flux_day_zero_manifests" { + type = bool + description = "Render day-zero Flux manifests from the existing clusters/atlas/flux-system artifacts." + default = false +} + +variable "apply_flux_day_zero_manifests" { + type = bool + description = "Dangerous: apply rendered day-zero Flux manifests with kubectl. Keep false for steady state." + default = false +} + +variable "flux_day_zero_output_path" { + type = string + description = "Rendered Flux day-zero manifest path when render_flux_day_zero_manifests is true." + default = "generated/flux-day-zero.yaml" +} diff --git a/terraform/atlas/versions.tf b/terraform/atlas/versions.tf new file mode 100644 index 000000000..17fe5d845 --- /dev/null +++ b/terraform/atlas/versions.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">= 1.6.0" + + backend "local" { + path = "terraform.tfstate" + } + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 3.2" + } + local = { + source = "hashicorp/local" + version = "~> 2.5" + } + } +} diff --git a/terraform/modules/atlas_inventory_labels/main.tf b/terraform/modules/atlas_inventory_labels/main.tf new file mode 100644 index 000000000..c707b12a3 --- /dev/null +++ b/terraform/modules/atlas_inventory_labels/main.tf @@ -0,0 +1,74 @@ +locals { + control_planes = sort([ + for name, node in var.nodes : name + if node.role == "control-plane" + ]) + + workers = sort([ + for name, node in var.nodes : name + if node.role == "worker" + ]) + + ssh_managed_nodes = sort([ + for name, node in var.nodes : name + if node.managed + ]) + + ssh_node_hosts = { + for name, node in var.nodes : name => node.ip + if node.managed + } + + ssh_node_users = { + for name, node in var.nodes : name => node.ssh_user + if node.managed && try(node.ssh_user, null) != null && node.ssh_user != var.default_ssh_user + } + + required_node_labels = { + for name, node in var.nodes : name => node.labels + if length(node.labels) > 0 + } + + ignore_unavailable_nodes = sort([ + for name, node in var.nodes : name + if node.ignore_unavailable + ]) + + ananke_inventory = { + ssh_user = var.default_ssh_user + ssh_port = var.default_ssh_port + ssh_config_file = var.default_ssh_config_file + ssh_identity_file = var.default_ssh_identity_file + ssh_node_hosts = local.ssh_node_hosts + ssh_node_users = local.ssh_node_users + ssh_managed_nodes = local.ssh_managed_nodes + control_planes = local.control_planes + workers = local.workers + startup = { + required_node_labels_mode = "validate" + required_node_labels = local.required_node_labels + ignore_unavailable_nodes = local.ignore_unavailable_nodes + } + } +} + +resource "kubernetes_labels" "node" { + for_each = local.required_node_labels + + api_version = "v1" + kind = "Node" + field_manager = var.label_field_manager + force = var.force_node_label_conflicts + + metadata { + name = each.key + } + + labels = each.value +} + +resource "local_file" "ananke_inventory_fragment" { + filename = var.ananke_inventory_fragment_path + file_permission = "0640" + content = "# Generated by Terraform. Do not add secrets here.\n${yamlencode(local.ananke_inventory)}" +} diff --git a/terraform/modules/atlas_inventory_labels/outputs.tf b/terraform/modules/atlas_inventory_labels/outputs.tf new file mode 100644 index 000000000..97fe311cd --- /dev/null +++ b/terraform/modules/atlas_inventory_labels/outputs.tf @@ -0,0 +1,27 @@ +output "control_planes" { + value = local.control_planes +} + +output "workers" { + value = local.workers +} + +output "ssh_managed_nodes" { + value = local.ssh_managed_nodes +} + +output "ssh_node_hosts" { + value = local.ssh_node_hosts +} + +output "required_node_labels" { + value = local.required_node_labels +} + +output "ignore_unavailable_nodes" { + value = local.ignore_unavailable_nodes +} + +output "ananke_inventory_fragment_path" { + value = local_file.ananke_inventory_fragment.filename +} diff --git a/terraform/modules/atlas_inventory_labels/variables.tf b/terraform/modules/atlas_inventory_labels/variables.tf new file mode 100644 index 000000000..d06809eb7 --- /dev/null +++ b/terraform/modules/atlas_inventory_labels/variables.tf @@ -0,0 +1,49 @@ +variable "nodes" { + type = map(object({ + ip = string + role = string + managed = optional(bool, true) + ignore_unavailable = optional(bool, false) + ssh_user = optional(string) + ssh_port = optional(number) + ssh_config_file = optional(string) + ssh_identity_file = optional(string) + labels = optional(map(string), {}) + })) + description = "Atlas host and Kubernetes node inventory." +} + +variable "default_ssh_user" { + type = string + description = "Default SSH user for generated Ananke inventory." +} + +variable "default_ssh_port" { + type = number + description = "Default SSH port for generated Ananke inventory." +} + +variable "default_ssh_config_file" { + type = string + description = "Default SSH config file for generated Ananke inventory." +} + +variable "default_ssh_identity_file" { + type = string + description = "Default SSH identity file for generated Ananke inventory." +} + +variable "ananke_inventory_fragment_path" { + type = string + description = "Generated Ananke inventory fragment path." +} + +variable "label_field_manager" { + type = string + description = "Kubernetes server-side apply field manager for node labels." +} + +variable "force_node_label_conflicts" { + type = bool + description = "Force overwriting label field ownership conflicts during planned adoption." +} diff --git a/terraform/modules/flux_day_zero_render/main.tf b/terraform/modules/flux_day_zero_render/main.tf new file mode 100644 index 000000000..cf7d7c7dc --- /dev/null +++ b/terraform/modules/flux_day_zero_render/main.tf @@ -0,0 +1,38 @@ +data "local_file" "gotk_components" { + count = var.enabled ? 1 : 0 + filename = "${var.flux_system_path}/gotk-components.yaml" +} + +data "local_file" "gotk_sync" { + count = var.enabled ? 1 : 0 + filename = "${var.flux_system_path}/gotk-sync.yaml" +} + +locals { + rendered_manifest = var.enabled ? join("\n---\n", [ + data.local_file.gotk_components[0].content, + data.local_file.gotk_sync[0].content, + ]) : "" +} + +resource "local_file" "rendered" { + count = var.enabled ? 1 : 0 + filename = var.output_path + file_permission = "0640" + content = local.rendered_manifest +} + +resource "terraform_data" "apply_rendered" { + count = var.enabled && var.apply_manifests ? 1 : 0 + + triggers_replace = { + rendered_sha256 = sha256(local.rendered_manifest) + } + + provisioner "local-exec" { + command = "kubectl apply --server-side -f ${local_file.rendered[0].filename}" + environment = { + KUBECONFIG = pathexpand(var.kubeconfig_path) + } + } +} diff --git a/terraform/modules/flux_day_zero_render/outputs.tf b/terraform/modules/flux_day_zero_render/outputs.tf new file mode 100644 index 000000000..96f7d84e8 --- /dev/null +++ b/terraform/modules/flux_day_zero_render/outputs.tf @@ -0,0 +1,3 @@ +output "rendered_manifest_path" { + value = var.enabled ? local_file.rendered[0].filename : null +} diff --git a/terraform/modules/flux_day_zero_render/variables.tf b/terraform/modules/flux_day_zero_render/variables.tf new file mode 100644 index 000000000..ce845007d --- /dev/null +++ b/terraform/modules/flux_day_zero_render/variables.tf @@ -0,0 +1,26 @@ +variable "enabled" { + type = bool + description = "Render day-zero Flux manifests from existing repo artifacts." + default = false +} + +variable "apply_manifests" { + type = bool + description = "Apply rendered Flux day-zero manifests with kubectl. Intended only for empty-cluster bootstrap." + default = false +} + +variable "kubeconfig_path" { + type = string + description = "Kubeconfig used only when apply_manifests is true." +} + +variable "flux_system_path" { + type = string + description = "Path to clusters/atlas/flux-system." +} + +variable "output_path" { + type = string + description = "Rendered day-zero Flux manifest output path." +}