diff --git a/terraform/.gitignore b/terraform/.gitignore index 2ab921d..3166e9d 100644 --- a/terraform/.gitignore +++ b/terraform/.gitignore @@ -11,7 +11,8 @@ crash.log crash.*.log # Ignore local Terraform configuration files that can contain sensitive data. -#terraform.tfvars +terraform.tfvars +.maidn/ # Ignore override files as they are typically used for local testing. # These should not be checked in to source control. @@ -58,4 +59,4 @@ terraform.rc .DS_Store Thumbs.db /.secrets -/test \ No newline at end of file +/test diff --git a/terraform/locals.tf b/terraform/locals.tf index cbb3abc..b222d4f 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -11,7 +11,7 @@ locals { all_networks_flat = flatten([ for node in var.nodes : [ for network in node.networks : { - proxmox_node = lookup(node, "proxmox_node", var.proxmox_node) + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node) vlan_id = network.vlan_id } ] @@ -57,10 +57,10 @@ locals { cores = node.cores memory = node.memory role = node.role - proxmox_node = lookup(node, "proxmox_node", var.proxmox_node) + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node) disk_size = node.disk_size disk_storage = var.disk_storage - tags = lookup(node, "tags", [node.role]) + tags = coalesce(node.tags, [node.role]) iso_file = local.node_iso_files[node.name] additional_disk_size = lookup(node, "additional_disk_size", null) additional_disk_storage = lookup(node, "additional_disk_size", null) != null ? var.additional_disk_storage : null @@ -90,4 +90,4 @@ locals { description = "Talos Worker Node - Managed by Terraform" } } -} \ No newline at end of file +} diff --git a/terraform/main.tf b/terraform/main.tf index 468e3f3..6c94f58 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -21,7 +21,7 @@ resource "null_resource" "stage_talos_image" { triggers = { node_name = each.value.name - proxmox_node = lookup(each.value, "proxmox_node", var.proxmox_node) + proxmox_node = coalesce(each.value.proxmox_node, var.proxmox_node) image_url = local.talos_iso_download_url storage = var.talos_image_storage talos_version = var.talos_version @@ -31,19 +31,26 @@ resource "null_resource" "stage_talos_image" { provisioner "local-exec" { interpreter = ["python", "-c"] - command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--api-token', '${var.proxmox_api_token}', '--node', '${self.triggers.proxmox_node}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}', '--force'], check=True)" + environment = { + MAIDN_PROXMOX_API_TOKEN = var.proxmox_api_token + } + command = "import subprocess; subprocess.run(['python', r'${path.module}/scripts/stage_talos_image.py', '--api-url', '${var.proxmox_api_url}', '--node', '${self.triggers.proxmox_node}', '--storage', '${var.talos_image_storage}', '--image-url', '${local.talos_iso_download_url}', '--filename', '${self.triggers.filename}'], check=True)" } } # Create all required network bridges resource "proxmox_virtual_environment_network_linux_bridge" "cluster_bridge" { - for_each = local.unique_bridges + for_each = var.manage_network_bridges ? local.unique_bridges : {} node_name = each.value.proxmox_node name = each.value.bridge_name comment = "Auto-created bridge for Talos VLAN ${each.value.vlan_id} on ${each.value.proxmox_node}" ports = var.create_vlan_interface ? ["${each.value.phys_iface}.${each.value.vlan_id}"] : [each.value.phys_iface] vlan_aware = false + + lifecycle { + prevent_destroy = true + } } # Create the Virtual Machines diff --git a/terraform/scripts/stage_talos_image.py b/terraform/scripts/stage_talos_image.py index 7899803..9b08fe5 100644 --- a/terraform/scripts/stage_talos_image.py +++ b/terraform/scripts/stage_talos_image.py @@ -1,5 +1,6 @@ import argparse import json +import os import ssl import subprocess import sys @@ -82,13 +83,15 @@ def wait_for_storage_content(base_url, token, node, storage, filename): def main(): parser = argparse.ArgumentParser() parser.add_argument("--api-url", required=True) - parser.add_argument("--api-token", required=True) + parser.add_argument("--api-token", default=os.environ.get("MAIDN_PROXMOX_API_TOKEN")) parser.add_argument("--node", required=True) parser.add_argument("--storage", required=True) parser.add_argument("--image-url", required=True) parser.add_argument("--filename", required=True) parser.add_argument("--force", action="store_true") args = parser.parse_args() + if not args.api_token: + parser.error("--api-token or MAIDN_PROXMOX_API_TOKEN is required") try: filename, downloaded = ensure_image(args.api_url, args.api_token, args.node, args.storage, args.image_url, args.filename, args.force) diff --git a/terraform/talconfig_generator.tf b/terraform/talconfig_generator.tf index 379cc44..ab1255a 100644 --- a/terraform/talconfig_generator.tf +++ b/terraform/talconfig_generator.tf @@ -7,6 +7,8 @@ resource "local_file" "talconfig" { cluster_endpoint = local.cluster_endpoint cluster_domain = var.cluster_domain talos_version = var.talos_version + kubernetes_version = var.kubernetes_version + architecture = var.talos_image_architecture talos_factory_schematic_id = var.talos_factory_schematic_id control_plane_vip = var.control_plane_vip @@ -20,34 +22,34 @@ resource "local_file" "talconfig" { name = node.name, role = node.role, networks = node.networks, - tags = node.tags, - proxmox_node = node.proxmox_node + tags = coalesce(node.tags, []), + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node) } if node.role == "controlplane"] compute_workers = [for node in var.nodes : { name = node.name, role = node.role, networks = node.networks, - tags = node.tags, - proxmox_node = node.proxmox_node - } if node.role == "worker" && contains(lookup(node, "tags", []), "compute")] + tags = coalesce(node.tags, []), + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node) + } if node.role == "worker" && !contains(coalesce(node.tags, []), "storage") && !contains(coalesce(node.tags, []), "gpu")] storage_workers = [for node in var.nodes : { name = node.name, role = node.role, networks = node.networks, - tags = node.tags, - proxmox_node = node.proxmox_node, + tags = coalesce(node.tags, []), + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node), additional_disk_size = lookup(node, "additional_disk_size", "") - } if node.role == "worker" && contains(lookup(node, "tags", []), "storage")] + } if node.role == "worker" && contains(coalesce(node.tags, []), "storage")] gpu_workers = [for node in var.nodes : { name = node.name, role = node.role, networks = node.networks, - tags = node.tags, - proxmox_node = node.proxmox_node - } if node.role == "worker" && contains(lookup(node, "tags", []), "gpu")] + tags = coalesce(node.tags, []), + proxmox_node = coalesce(node.proxmox_node, var.proxmox_node) + } if node.role == "worker" && contains(coalesce(node.tags, []), "gpu")] # Other variables cluster_pod_nets = var.cluster_pod_nets @@ -63,16 +65,16 @@ output "node_organization" { description = "How your nodes are organized for Talos configuration" value = { control_plane_nodes = [for node in var.nodes : { - name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) + name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, []) } if node.role == "controlplane"] compute_workers = [for node in var.nodes : { - name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) - } if node.role == "worker" && contains(lookup(node, "tags", []), "compute")] + name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, []) + } if node.role == "worker" && !contains(coalesce(node.tags, []), "storage") && !contains(coalesce(node.tags, []), "gpu")] storage_workers = [for node in var.nodes : { - name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []), additional_disk = lookup(node, "additional_disk_size", null) - } if node.role == "worker" && contains(lookup(node, "tags", []), "storage")] + name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, []), additional_disk = node.additional_disk_size + } if node.role == "worker" && contains(coalesce(node.tags, []), "storage")] gpu_workers = [for node in var.nodes : { - name = node.name, ip = node.networks[0].ip, tags = lookup(node, "tags", []) - } if node.role == "worker" && contains(lookup(node, "tags", []), "gpu")] + name = node.name, ip = node.networks[0].ip, tags = coalesce(node.tags, []) + } if node.role == "worker" && contains(coalesce(node.tags, []), "gpu")] } } diff --git a/terraform/templates/talconfig.yaml.tpl b/terraform/templates/talconfig.yaml.tpl index 671885c..17db103 100644 --- a/terraform/templates/talconfig.yaml.tpl +++ b/terraform/templates/talconfig.yaml.tpl @@ -2,6 +2,7 @@ clusterName: ${cluster_name} endpoint: ${cluster_endpoint} domain: ${cluster_domain} talosVersion: ${talos_version} +kubernetesVersion: "${kubernetes_version}" cniConfig: name: ${cni_name} allowSchedulingOnMasters: ${length(control_plane_nodes) == 1 ? "true" : "false"} @@ -26,7 +27,7 @@ nodes: installDisk: /dev/sda machineSpec: mode: metal - arch: amd64 + arch: ${architecture} talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" patches: - |- @@ -70,7 +71,7 @@ nodes: installDisk: /dev/sda machineSpec: mode: metal - arch: amd64 + arch: ${architecture} talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" patches: - |- @@ -111,7 +112,7 @@ nodes: installDisk: /dev/sda machineSpec: mode: metal - arch: amd64 + arch: ${architecture} talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" patches: - |- @@ -152,7 +153,7 @@ nodes: installDisk: /dev/sda machineSpec: mode: metal - arch: amd64 + arch: ${architecture} talosImageURL: "factory.talos.dev/installer/${talos_factory_schematic_id}" patches: - |- diff --git a/terraform/variables.tf b/terraform/variables.tf index 74f1f6c..a68215b 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -50,6 +50,12 @@ variable "cni_name" { default = "none" } +variable "kubernetes_version" { + description = "The Kubernetes version rendered into Talos configuration." + type = string + default = "v1.33.4" +} + # --- Global Network Configuration --- variable "dns_servers" { description = "Global DNS servers for the cluster" @@ -133,7 +139,7 @@ variable "nodes" { mac_address = string cidr = string vlan_id = number - ip = optional(string) # Static IP, required for control plane, optional for workers + ip = optional(string) # Static IP required for every rendered Talos node gateway = optional(string) # Should only be defined on the primary interface })) })) @@ -144,11 +150,17 @@ variable "nodes" { } validation { - condition = alltrue([for node in var.nodes : node.role == "controlplane" ? node.networks[0].ip != null : true]) - error_message = "The first network for a 'controlplane' node must have a static IP address." + condition = alltrue([for node in var.nodes : node.networks[0].ip != null]) + error_message = "The first network for every node must have a static IP address." } } +variable "manage_network_bridges" { + description = "Whether Terraform owns Proxmox host bridges. Leave false for shared or pre-existing bridges." + type = bool + default = false +} + variable "default_physical_interface" { description = "Default physical interface for nodes (fallback)" type = string