From 748e1763ad190af82274415cd45e05254e7bc18d Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Tue, 8 Jul 2025 12:22:24 +0200 Subject: [PATCH] Test bringing up openstack instance --- .gitignore | 2 - .../khs.codes/pre/.terraform.lock.hcl | 47 -- flake.nix | 12 + nix/lib/sanitize-terraform-name/default.nix | 2 +- .../nixos/hetzner-instance/default.nix | 26 +- .../nixos/khs-openstack-instance/default.nix | 245 ++++++++++ nix/modules/nixos/openstack/default.nix | 1 + nix/modules/nixos/provisioning/default.nix | 2 +- nix/modules/terranix/hcloud/default.nix | 2 +- nix/modules/terranix/openstack/default.nix | 454 ++++++++++++++++++ nix/modules/terranix/openstack/output.nix | 51 ++ nix/modules/terranix/s3/default.nix | 33 ++ nix/modules/terranix/unifi/default.nix | 23 + nix/modules/terranix/unifi/output.nix | 12 + nix/packages/bw-opentofu/default.nix | 21 +- nix/packages/instance-opentofu/default.nix | 22 + nix/packages/opentofu/default.nix | 8 +- nix/packages/pre-provisioning/default.nix | 14 +- .../terraform-provider-cloudflare/default.nix | 10 + .../terraform-provider-hcloud/default.nix | 10 + .../terraform-provider-openstack/default.nix | 10 + nix/profiles/hetzner-server.nix | 3 +- nix/profiles/khs-openstack-server.nix | 7 + .../test.kaareskovgaard.net/default.nix | 14 + 24 files changed, 932 insertions(+), 99 deletions(-) delete mode 100644 .terraform-cache/khs.codes/pre/.terraform.lock.hcl create mode 100644 nix/modules/nixos/khs-openstack-instance/default.nix create mode 100644 nix/modules/nixos/openstack/default.nix create mode 100644 nix/modules/terranix/openstack/default.nix create mode 100644 nix/modules/terranix/openstack/output.nix create mode 100644 nix/modules/terranix/s3/default.nix create mode 100644 nix/modules/terranix/unifi/default.nix create mode 100644 nix/modules/terranix/unifi/output.nix create mode 100644 nix/packages/instance-opentofu/default.nix create mode 100644 nix/packages/terraform-provider-cloudflare/default.nix create mode 100644 nix/packages/terraform-provider-hcloud/default.nix create mode 100644 nix/packages/terraform-provider-openstack/default.nix create mode 100644 nix/profiles/khs-openstack-server.nix create mode 100644 nix/systems/x86_64-linux/test.kaareskovgaard.net/default.nix diff --git a/.gitignore b/.gitignore index ccdf78e..f18e503 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ .direnv/ result/ .DS_Store -.terraform-cache/*/*/config.tf.json -.terraform-cache/*/*/.terraform rust/target diff --git a/.terraform-cache/khs.codes/pre/.terraform.lock.hcl b/.terraform-cache/khs.codes/pre/.terraform.lock.hcl deleted file mode 100644 index 8dc594b..0000000 --- a/.terraform-cache/khs.codes/pre/.terraform.lock.hcl +++ /dev/null @@ -1,47 +0,0 @@ -# This file is maintained automatically by "tofu init". -# Manual edits may be lost in future updates. - -provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.52.0" - constraints = "~> 4.0" - hashes = [ - "h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=", - "zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0", - "zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8", - "zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238", - "zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f", - "zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f", - "zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8", - "zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9", - "zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060", - "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6", - "zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb", - "zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b", - "zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380", - "zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7", - "zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672", - ] -} - -provider "registry.opentofu.org/hetznercloud/hcloud" { - version = "1.45.0" - constraints = "~> 1.45.0" - hashes = [ - "h1:dh2iL5GHfDui5DbZFD/kcWlwzmC6slgUirA0FbZBK7g=", - "zh:1c4b44a698cfaca215bdbadaf92669dd23533210c3cbf32895fbf4ff7acf6c24", - "zh:2915f8385559694e5097d8d0df16358200e9f0d9efb80559e9ea0bd072d792b9", - "zh:3a6b37b0bba50d263bd3dba26185bde13c825e59b6b301ab3f9f45686a21456b", - "zh:3e3910fa22a3a8d73d1aed38cc479c3e1958e9168b5f4a7d0da6cf03c2dfc155", - "zh:3f8d7d09e5c93162a1e9e6c89acac0799fb55765b44b7d1d020763c814263c57", - "zh:40bc5e94bff495440e1b4f797165d7f0dcee2282a86a61b158f47fe4bc57e9fb", - "zh:473f51d464b897d0e8e3d5ca2eb175b37e2f7ce03c8b26f47cc35885cf620946", - "zh:6fdd4bf71c19cfad78d7e1d2336be873eb8567a139d53e672e78ebcbc36a4d7d", - "zh:9e08638cbfc90d69f1c21ee34191db077d58d040cf7a9eed07a1dc335d463e97", - "zh:b1ed5ea81bc6d2c88efdefaeb244322874508d90d8217ac2e3541445254bdadc", - "zh:ced05776c27d550d15d4a71360243740ecb4ea1e65e67229fb2273a27353b00c", - "zh:da79b8a1a982a1d365ea206a2654e8b5003aeba9ccdc9c8751bb6ee3f40d8c49", - "zh:fabbad25bab09dd74f2b819992ab99b939c642374d6ca080b18d6e2a91d8d487", - "zh:fb0e083d2925f289999dc561ef1c2f84a9e0ab11388c40162ca8b470f50f71f5", - ] -} diff --git a/flake.nix b/flake.nix index cd3a68a..adc20a1 100644 --- a/flake.nix +++ b/flake.nix @@ -86,5 +86,17 @@ inherit inputs; khscodesLib = inputs.self.lib; }; + terranixModules.s3 = import ./nix/modules/terranix/s3 { + inherit inputs; + khscodesLib = inputs.self.lib; + }; + terranixModules.openstack = import ./nix/modules/terranix/openstack { + inherit inputs; + khscodesLib = inputs.self.lib; + }; + terranixModules.unifi = import ./nix/modules/terranix/unifi { + inherit inputs; + khscodesLib = inputs.self.lib; + }; }; } diff --git a/nix/lib/sanitize-terraform-name/default.nix b/nix/lib/sanitize-terraform-name/default.nix index 9c0cf9a..695a075 100644 --- a/nix/lib/sanitize-terraform-name/default.nix +++ b/nix/lib/sanitize-terraform-name/default.nix @@ -1,4 +1,4 @@ { ... }: { - sanitize-terraform-name = name: builtins.replaceStrings [ "." ] [ "_" ] name; + sanitize-terraform-name = name: builtins.replaceStrings [ "." ":" "/" ] [ "_" "_" "_" ] name; } diff --git a/nix/modules/nixos/hetzner-instance/default.nix b/nix/modules/nixos/hetzner-instance/default.nix index 72b0237..e9a1e72 100644 --- a/nix/modules/nixos/hetzner-instance/default.nix +++ b/nix/modules/nixos/hetzner-instance/default.nix @@ -2,7 +2,6 @@ config, lib, inputs, - pkgs, ... }: let @@ -75,11 +74,6 @@ in description = "The Hetzner datacenter to create a server in"; default = "hel1-dc2"; }; - output = lib.mkOption { - type = lib.types.nullOr lib.types.package; - description = "The terranix package built from the configuration"; - default = null; - }; mapRdns = lib.mkOption { type = lib.types.bool; description = "Sets up RDNS for the server"; @@ -148,23 +142,13 @@ in imports = [ inputs.self.terranixModules.cloudflare inputs.self.terranixModules.hcloud + inputs.self.terranixModules.s3 ]; config = { - terraform.backend.s3 = { - bucket = "bw-terraform"; - key = cfg.bucket.key; - region = "auto"; - endpoints = { - s3 = "https://477b394a6a545699445c40953e40f00b.r2.cloudflarestorage.com"; - }; - use_path_style = true; - skip_credentials_validation = true; - skip_region_validation = true; - skip_metadata_api_check = true; - skip_requesting_account_id = true; - skip_s3_checksum = true; + khscodes.s3 = { + enable = true; + bucket.key = cfg.bucket.key; }; - khscodes.hcloud.data.ssh_key.khs = { name = "ca.kaareskovgaard.net"; }; @@ -229,7 +213,7 @@ in khscodes.provisioning.pre = { modules = modules; secretsSource = cfg.secretsSource; - endspoints = [ + endpoints = [ "aws" "cloudflare" "hcloud" diff --git a/nix/modules/nixos/khs-openstack-instance/default.nix b/nix/modules/nixos/khs-openstack-instance/default.nix new file mode 100644 index 0000000..ee58e5e --- /dev/null +++ b/nix/modules/nixos/khs-openstack-instance/default.nix @@ -0,0 +1,245 @@ +{ + config, + lib, + inputs, + ... +}: +let + cfg = config.khscodes.khs-openstack-instance; + fqdn = config.khscodes.fqdn; + firewallTcpRules = lib.lists.flatten ( + lib.lists.map (p: [ + { + direction = "ingress"; + ethertype = "IPv4"; + protocol = "tcp"; + port = p; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "ingress"; + ethertype = "IPv6"; + protocol = "tcp"; + port = p; + remote_subnet = "::/0"; + } + ]) config.networking.firewall.allowedTCPPorts + ); + firewallUdpRules = lib.lists.flatten ( + lib.lists.map (p: [ + { + direction = "ingress"; + ethertype = "IPv4"; + protocol = "udp"; + port = p; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "ingress"; + ethertype = "IPv6"; + protocol = "udp"; + port = p; + remote_subnet = "::/0"; + } + ]) config.networking.firewall.allowedUDPPorts + ); + firewallIcmpRules = lib.lists.optionals config.networking.firewall.allowPing [ + { + direction = "ingress"; + ethertype = "IPv4"; + protocol = "icmp"; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "ingress"; + ethertype = "IPv6"; + protocol = "icmp"; + remote_subnet = "::/0"; + } + ]; + firewallRules = firewallTcpRules ++ firewallUdpRules ++ firewallIcmpRules ++ cfg.extraFirewallRules; + tldFromFqdn = + fqdn: + let + split = lib.strings.splitString "." fqdn; + in + if lib.lists.length split < 3 then + fqdn + else + lib.strings.removePrefix "${builtins.head split}." fqdn; +in +{ + options.khscodes.khs-openstack-instance = { + enable = lib.mkEnableOption "enables generating a opentofu config for khs openstack instance"; + dnsNames = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "DNS names for the instance"; + default = [ fqdn ]; + }; + bucket = { + key = lib.mkOption { + type = lib.types.str; + description = "Key for use in the bucket"; + default = "${fqdn}.tfstate"; + }; + }; + secretsSource = lib.mkOption { + type = lib.types.enum [ + "bitwarden" + "vault" + ]; + description = "Whether to load opentofu secrets from Bitwarden or Vault"; + default = "vault"; + }; + flavor = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "The server type to create"; + default = null; + }; + ssh_key = lib.mkOption { + type = lib.types.str; + description = "SSH key for the server (this only applies to the initial creation, deploying NixOS will render this key useless). Changing this will recreate the instance"; + default = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqY0FHnWFKfLG2yfgr4qka5sR9CK+EMAhzlHUkaQyWHTKD+G0/vC/fNPyL1VV3Dxc/ajxGuPzVE+mBMoyxazL3EtuCDOVvHJ5CR+MUSEckg/DDwcGHqy6rC8BvVVpTAVL04ByQdwFnpE1qNSBaQLkxaFVdtriGKkgMkc7+UNeYX/bv7yn+APqfP1a3xr6wdkSSdO8x4N2jsSygOIMx10hLyCV4Ueu7Kp8Ww4rGY8j5o7lKJhbgfItBfSOuQHdppHVF/GKYRhdnK6Y2fZVYbhq4KipUtclbZ6O/VYd8/sOO98+LMm7cOX+K35PQjUpYgcoNy5+Sw3CNS/NHn4JvOtTaUEYP7fK6c9LhMULOO3T7Cm6TMdiFjUKHkyG+s2Mu/LXJJoilw571zwuh6chkeitW8+Ht7k0aPV96kNEvTdoXwLhBifVEaChlAsLAzSUjUq+YYCiXVk0VIXCZQWKj8LoVNTmaqDksWwbcT64fw/FpVC0N18WHbKcFUEIW/O4spJMa30CQwf9FeqpoWoaF1oRClCSDPvX0AauCu0JcmRinz1/JmlXljnXWbSfm20/V+WyvktlI0wTD0cdpNuSasT9vS77YfJ8nutcWWZKSkCj4R4uHeCNpDTX5YXzapy7FxpM9ANCXLIvoGX7Yafba2Po+er7SSsUIY1AsnBBr8ZoDVw=="; + }; + extraFirewallRules = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + description = "Extra firewall rules added to the instance"; + default = [ + { + direction = "egress"; + ethertype = "IPv4"; + protocol = "tcp"; + port = 80; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "egress"; + ethertype = "IPv6"; + protocol = "tcp"; + port = 80; + remote_subnet = "::/0"; + } + { + direction = "egress"; + ethertype = "IPv4"; + protocol = "tcp"; + port = 443; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "egress"; + ethertype = "IPv6"; + protocol = "tcp"; + port = 443; + remote_subnet = "::/0"; + } + { + direction = "egress"; + ethertype = "IPv4"; + protocol = "udp"; + port = 443; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "egress"; + ethertype = "IPv6"; + protocol = "udp"; + port = 443; + remote_subnet = "::/0"; + } + { + direction = "egress"; + ethertype = "IPv4"; + protocol = "icmp"; + remote_subnet = "0.0.0.0/0"; + } + { + direction = "egress"; + ethertype = "IPv6"; + protocol = "icmp"; + remote_subnet = "::/0"; + } + ]; + }; + }; + config = lib.mkIf cfg.enable ( + let + tags = [ fqdn ]; + modules = [ + ( + { config, ... }: + { + imports = [ + inputs.self.terranixModules.cloudflare + inputs.self.terranixModules.openstack + inputs.self.terranixModules.unifi + inputs.self.terranixModules.s3 + ]; + config = { + khscodes.s3 = { + enable = true; + bucket.key = cfg.bucket.key; + }; + khscodes.openstack.enable = true; + khscodes.openstack.compute_instance.compute = { + inherit tags; + name = fqdn; + initial_image = "Ubuntu-22.04"; + flavor = cfg.flavor; + ssh_public_key = cfg.ssh_key; + firewall_rules = firewallRules; + }; + khscodes.cloudflare = { + enable = true; + dns = { + enable = true; + zone_name = tldFromFqdn fqdn; + aRecords = [ + { + inherit fqdn; + content = config.khscodes.openstack.output.compute_instance.compute.ipv4_address; + } + ]; + aaaaRecords = [ + { + inherit fqdn; + content = config.khscodes.openstack.output.compute_instance.compute.ipv6_address; + } + ]; + }; + }; + output.ipv4_address = { + value = config.khscodes.openstack.output.compute_instance.compute.ipv4_address; + sensitive = false; + }; + + output.ipv6_address = { + value = config.khscodes.openstack.output.compute_instance.compute.ipv6_address; + sensitive = false; + }; + }; + } + ) + ]; + in + { + assertions = [ + { + assertion = config.khscodes.fqdn != null; + message = "Must set config.khscodes.fqdn when using opentofu"; + } + ]; + + khscodes.provisioning.pre = { + modules = modules; + secretsSource = cfg.secretsSource; + endpoints = [ + "aws" + "cloudflare" + "openstack" + "unifi" + ]; + }; + } + ); +} diff --git a/nix/modules/nixos/openstack/default.nix b/nix/modules/nixos/openstack/default.nix new file mode 100644 index 0000000..c915eb0 --- /dev/null +++ b/nix/modules/nixos/openstack/default.nix @@ -0,0 +1 @@ +{ ... }: { } diff --git a/nix/modules/nixos/provisioning/default.nix b/nix/modules/nixos/provisioning/default.nix index 5a58ded..c76cb11 100644 --- a/nix/modules/nixos/provisioning/default.nix +++ b/nix/modules/nixos/provisioning/default.nix @@ -21,7 +21,7 @@ let description = "Where to get the secrets for the provisioning from"; default = "vault"; }; - endspoints = lib.mkOption { + endpoints = lib.mkOption { type = lib.types.listOf ( lib.types.enum [ "openstack" diff --git a/nix/modules/terranix/hcloud/default.nix b/nix/modules/terranix/hcloud/default.nix index 49761fd..039c649 100644 --- a/nix/modules/terranix/hcloud/default.nix +++ b/nix/modules/terranix/hcloud/default.nix @@ -88,7 +88,7 @@ in config = lib.mkIf cfg.enable { hcloud.enable = true; - terraform.required_providers.hcloud.version = "~> 1.45.0"; + terraform.required_providers.hcloud.version = "~> 1.51.0"; resource.hcloud_server = mapSanitizedAttrs ( { name, value }: { diff --git a/nix/modules/terranix/openstack/default.nix b/nix/modules/terranix/openstack/default.nix new file mode 100644 index 0000000..ba13dba --- /dev/null +++ b/nix/modules/terranix/openstack/default.nix @@ -0,0 +1,454 @@ +{ khscodesLib, inputs }: +{ lib, config, ... }: +let + cfg = config.khscodes.openstack; + modules = [ + ./output.nix + ]; + firewallRuleModule = khscodesLib.mkSubmodule { + description = "Firewall rule"; + options = { + direction = lib.mkOption { + type = lib.types.enum [ + "ingress" + "egress" + ]; + description = "The direction of the firewall rule"; + }; + ethertype = lib.mkOption { + type = lib.types.enum [ + "IPv4" + "IPv6" + ]; + description = "The IP version"; + }; + protocol = lib.mkOption { + type = lib.types.enum [ + "tcp" + "udp" + "icmp" + ]; + description = "The protocol"; + }; + port = lib.mkOption { + type = lib.types.int; + description = "The port (for udp and tcp rules) to apply the rule to"; + }; + remote_subnet = lib.mkOption { + type = lib.types.str; + description = "The remote subnet to apply the rule to"; + }; + }; + }; + mapFirewallRule = + security_group_id: rule: + { + inherit (rule) direction ethertype; + inherit security_group_id; + protocol = + if rule.ethertype == "IPv6" && rule.protocol == "icmp" then "ipv6-icmp" else rule.protocol; + remote_ip_prefix = rule.remote_subnet; + } + // (lib.attrsets.optionalAttrs (rule.protocol != "icmp") { + port_range_min = rule.port; + port_range_max = rule.port; + }); + openstackComputeInstance = khscodesLib.mkSubmodule { + description = "Openstack compute instance"; + options = { + name = lib.mkOption { + type = lib.types.str; + description = "Name of the instance"; + }; + flavor = lib.mkOption { + type = lib.types.str; + description = "Flavor of the instance"; + }; + initial_image = lib.mkOption { + type = lib.types.str; + description = "Initial image of the server"; + }; + ssh_public_key = lib.mkOption { + type = lib.types.str; + description = "The ssh key added to the server"; + }; + ip4_cidr = lib.mkOption { + type = lib.types.str; + description = "IPv4 cidr of the private virtual network"; + default = "172.24.0.0/24"; + }; + ip4_dns_nameservers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "DNS (IPv4) nameservers to set (in DHCP) for the ipv4 subnet"; + default = [ + "1.1.1.1" + "1.0.0.1" + ]; + }; + volume_size = lib.mkOption { + type = lib.types.int; + description = "Size of the root volume, in gigabytes"; + default = 30; + }; + volume_type = lib.mkOption { + type = lib.types.enum [ + "Encrypted" + "__DEFAULT__" + ]; + description = "The type of volume to create"; + default = "Encrypted"; + }; + ip6_dns_nameservers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "DNS (IPv6) nameservers to set (in DHCP) for the ipv4 subnet"; + default = [ + "2606:4700:4700::1111" + "2606:4700:4700::1001" + ]; + }; + tags = lib.mkOption { + type = lib.types.listOf lib.types.str; + }; + firewall_rules = lib.mkOption { + type = lib.types.listOf firewallRuleModule; + description = "List of firewall rules to apply to the server"; + default = [ ]; + }; + }; + }; +in +{ + options.khscodes.openstack = { + enable = lib.mkEnableOption "Enables the openstack provider"; + compute_instance = lib.mkOption { + type = lib.types.attrsOf openstackComputeInstance; + description = "Defines an openstack compute instance"; + default = { }; + }; + }; + + imports = lib.lists.map (m: import m { inherit khscodesLib inputs; }) modules; + + config = lib.mkIf cfg.enable { + terraform.required_providers.openstack = { + source = "terraform-provider-openstack/openstack"; + version = "~> 3.2.0"; + }; + provider.openstack = { + user_name = "\${ var.openstack_username }"; + tenant_name = "\${ var.openstack_tenant_name }"; + password = "\${ var.openstack_password }"; + auth_url = "\${ var.openstack_auth_url }"; + region = "\${ var.openstack_region }"; + endpoint_type = "\${ var.openstack_endpoint_type }"; + }; + variable = { + openstack_username = { + type = "string"; + }; + openstack_password = { + type = "string"; + sensitive = true; + }; + openstack_tenant_name = { + type = "string"; + }; + openstack_auth_url = { + type = "string"; + }; + openstack_endpoint_type = { + type = "string"; + }; + openstack_region = { + type = "string"; + }; + }; + # DATA + + # flavor + data.openstack_compute_flavor_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.flavor; + }; + } + ) cfg.compute_instance; + + # image + data.openstack_images_image_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.initial_image; + }; + } + ) cfg.compute_instance; + + # provider network + data.openstack_networking_network_v2.provider = { + name = "public"; + }; + + # provider subnet pool (for ipv6) + data.openstack_networking_subnetpool_v2.provider = { + name = "provider-subnet-pool"; + }; + + # RESOURCE + + # keypair + resource.openstack_compute_keypair_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = khscodesLib.sanitize-terraform-name value.name; + public_key = value.ssh_public_key; + }; + } + ) cfg.compute_instance; + + # router + resource.openstack_networking_router_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.name; + external_network_id = "\${ data.openstack_networking_network_v2.provider.id }"; + tags = value.tags; + }; + } + ) cfg.compute_instance; + + # network + resource.openstack_networking_network_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.name; + tags = value.tags; + }; + } + ) cfg.compute_instance; + + # subnet + resource.openstack_networking_subnet_v2 = + (lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = "${sanitizedName}_ip4"; + value = { + name = "ip4: ${value.name}"; + cidr = value.ip4_cidr; + ip_version = 4; + network_id = "\${ openstack_networking_network_v2.${sanitizedName}.id }"; + dns_nameservers = value.ip4_dns_nameservers; + tags = value.tags; + }; + } + ) cfg.compute_instance) + // (lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = "${sanitizedName}_ip6"; + value = { + name = "ip6: ${value.name}"; + ip_version = 6; + ipv6_address_mode = "dhcpv6-stateless"; + ipv6_ra_mode = "dhcpv6-stateless"; + subnetpool_id = "\${ data.openstack_networking_subnetpool_v2.provider.id }"; + dns_nameservers = value.ip6_dns_nameservers; + network_id = "\${ openstack_networking_network_v2.${sanitizedName}.id }"; + tags = value.tags; + }; + } + ) cfg.compute_instance); + + # router interface + resource.openstack_networking_router_interface_v2 = + (lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = "${sanitizedName}_ip4"; + value = { + router_id = "\${ openstack_networking_router_v2.${sanitizedName}.id }"; + subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip4.id }"; + }; + } + ) cfg.compute_instance) + // (lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = "${sanitizedName}_ip6"; + value = { + router_id = "\${ openstack_networking_router_v2.${sanitizedName}.id }"; + subnet_id = "\${ openstack_networking_subnet_v2.${sanitizedName}_ip6.id }"; + }; + } + ) cfg.compute_instance); + + # floating ip + resource.openstack_networking_floatingip_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + pool = "public"; + tags = value.tags; + }; + } + ) cfg.compute_instance; + + # volume + resource.openstack_blockstorage_volume_v3 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.name; + size = value.volume_size; + image_id = "\${ data.openstack_images_image_v2.${sanitizedName}.id }"; + volume_type = value.volume_type; + }; + } + ) cfg.compute_instance; + + # security group + resource.openstack_networking_secgroup_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.name; + tags = value.tags; + }; + } + ) cfg.compute_instance; + + # security group rules (firewall rules) + resource.openstack_networking_secgroup_rule_v2 = lib.attrsets.mergeAttrsList ( + lib.lists.flatten ( + lib.lists.map ( + { name, value }: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + lib.listToAttrs ( + lib.lists.map ( + rule: + let + protocol = + if rule.protocol == "icmp" then "icmp" else "${rule.protocol}_${builtins.toString rule.port}"; + in + { + name = "${sanitizedName}_${rule.direction}_${rule.ethertype}_${protocol}_${khscodesLib.sanitize-terraform-name rule.remote_subnet}"; + value = mapFirewallRule "\${ resource.openstack_networking_secgroup_v2.${sanitizedName}.id }" rule; + } + ) value.firewall_rules + ) + ) (lib.attrsToList cfg.compute_instance) + ) + ); + + # instance + data.openstack_networking_port_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + device_id = "\${ openstack_compute_instance_v2.${sanitizedName}.id }"; + network_id = "\${ openstack_networking_network_v2.${sanitizedName}.id }"; + }; + } + ) cfg.compute_instance; + resource.openstack_compute_instance_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + name = value.name; + tags = value.tags; + flavor_id = "\${ data.openstack_compute_flavor_v2.${sanitizedName}.id }"; + key_pair = "\${ openstack_compute_keypair_v2.${sanitizedName}.name }"; + block_device = [ + { + uuid = "\${ openstack_blockstorage_volume_v3.${sanitizedName}.id }"; + source_type = "volume"; + boot_index = 0; + destination_type = "volume"; + delete_on_termination = false; + } + ]; + security_groups = [ "\${ openstack_networking_secgroup_v2.${sanitizedName}.name }" ]; + network = [ + { + uuid = "\${ openstack_networking_network_v2.${sanitizedName}.id }"; + } + ]; + }; + } + ) cfg.compute_instance; + + # floating ip associate + resource.openstack_networking_floatingip_associate_v2 = lib.mapAttrs' ( + name: value: + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + name = sanitizedName; + value = { + floating_ip = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }"; + port_id = "\${ data.openstack_networking_port_v2.${sanitizedName}.id }"; + }; + } + ) cfg.compute_instance; + }; +} diff --git a/nix/modules/terranix/openstack/output.nix b/nix/modules/terranix/openstack/output.nix new file mode 100644 index 0000000..da8c6a1 --- /dev/null +++ b/nix/modules/terranix/openstack/output.nix @@ -0,0 +1,51 @@ +{ khscodesLib, ... }: +{ config, lib, ... }: +let + cfg = config.khscodes.openstack; + openstackOutputInstanceModule = khscodesLib.mkSubmodule { + description = "Instance output"; + options = { + id = lib.mkOption { + type = lib.types.str; + description = "ID of the instance, as a terraform string expression"; + }; + ipv4_address = lib.mkOption { + type = lib.types.str; + description = "IPv4 address of the instance, as a terraform string expression"; + }; + ipv6_address = lib.mkOption { + type = lib.types.str; + description = "IPv6 address of the instance, as a terraform string expression"; + }; + ipv6_external_gateway = lib.mkOption { + type = lib.types.str; + description = "The IPv6 external gateway for the network. This is useful to eg. create static routes in Unifi"; + }; + }; + }; +in +{ + options.khscodes.openstack = { + output.compute_instance = lib.mkOption { + type = lib.types.attrsOf openstackOutputInstanceModule; + description = "Set by this module to be read by other modules when needing results of defining a compute instance"; + default = { }; + }; + }; + config = { + khscodes.openstack.output.compute_instance = lib.attrsets.mapAttrs ( + name: value: + ( + let + sanitizedName = khscodesLib.sanitize-terraform-name name; + in + { + id = "\${ openstack_compute_instance_v2.${sanitizedName}.id }"; + ipv4_address = "\${ openstack_networking_floatingip_v2.${sanitizedName}.address }"; + ipv6_address = "\${ data.openstack_networking_port_v2.${sanitizedName}.all_fixed_ips[1] }"; + ipv6_external_gateway = "\${ [for ip in openstack_networking_router_v2.${sanitizedName}.external_fixed_ip : ip.ip_address if replace(ip.ip_address, \":\", \"\") != ip.ip_address][0] }"; + } + ) + ) cfg.compute_instance; + }; +} diff --git a/nix/modules/terranix/s3/default.nix b/nix/modules/terranix/s3/default.nix new file mode 100644 index 0000000..c11c9f5 --- /dev/null +++ b/nix/modules/terranix/s3/default.nix @@ -0,0 +1,33 @@ +{ ... }: +{ lib, config, ... }: +let + cfg = config.khscodes.s3; +in +{ + options.khscodes.s3 = { + enable = lib.mkEnableOption "Enables the s3 backend"; + bucket = { + key = lib.mkOption { + type = lib.types.str; + description = "key for the bucket to use"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + terraform.backend.s3 = { + bucket = "bw-terraform"; + key = cfg.bucket.key; + region = "auto"; + endpoints = { + s3 = "https://477b394a6a545699445c40953e40f00b.r2.cloudflarestorage.com"; + }; + use_path_style = true; + skip_credentials_validation = true; + skip_region_validation = true; + skip_metadata_api_check = true; + skip_requesting_account_id = true; + skip_s3_checksum = true; + }; + }; +} diff --git a/nix/modules/terranix/unifi/default.nix b/nix/modules/terranix/unifi/default.nix new file mode 100644 index 0000000..d996b61 --- /dev/null +++ b/nix/modules/terranix/unifi/default.nix @@ -0,0 +1,23 @@ +{ khscodesLib, inputs }: +{ lib, config, ... }: +let + cfg = config.khscodes.unifi; + modules = [ + ./output.nix + ]; +in +{ + options.khscodes.unifi = { + enable = lib.mkEnableOption "Enables the unifi provider"; + bucket = { + key = lib.mkOption { + type = lib.types.str; + description = "key for the bucket to use"; + }; + }; + }; + + imports = lib.lists.map (m: import m { inherit khscodesLib inputs; }) modules; + + config = lib.mkIf cfg.enable { }; +} diff --git a/nix/modules/terranix/unifi/output.nix b/nix/modules/terranix/unifi/output.nix new file mode 100644 index 0000000..8288c92 --- /dev/null +++ b/nix/modules/terranix/unifi/output.nix @@ -0,0 +1,12 @@ +{ khscodesLib, ... }: +{ config, lib, ... }: +let + cfg = config.khscodes.unifi; +in +{ + options.khscodes.unifi = { + + }; + config = { + }; +} diff --git a/nix/packages/bw-opentofu/default.nix b/nix/packages/bw-opentofu/default.nix index 305c922..6cbeba7 100644 --- a/nix/packages/bw-opentofu/default.nix +++ b/nix/packages/bw-opentofu/default.nix @@ -5,26 +5,7 @@ let # for now this map just needs to include every secret we could need, which also makes the reading of secrets take way longer than # needed. secrets = import ./secrets-map.nix; - wrappedScript = pkgs.writeShellApplication { - name = "bw-opentofu-wrapped"; - runtimeInputs = [ - pkgs.uutils-coreutils-noprefix - pkgs.bitwarden-cli - pkgs.khscodes.find-flake-root - opentofu - ]; - text = '' - fqdn="$1" - config="$2" - phase="$3" - flakeRoot="$(find-flake-root)" - dir="$flakeRoot/.terraform-cache/$fqdn/$phase" - mkdir -p "$dir" - cat "''${config}" > "$dir/config.tf.json" - tofu -chdir="$dir" init - tofu -chdir="$dir" apply - ''; - }; + wrappedScript = pkgs.khscodes.instance-opentofu; in lib.khscodes.mkBwEnv { inherit pkgs; diff --git a/nix/packages/instance-opentofu/default.nix b/nix/packages/instance-opentofu/default.nix new file mode 100644 index 0000000..a959fae --- /dev/null +++ b/nix/packages/instance-opentofu/default.nix @@ -0,0 +1,22 @@ +{ pkgs, ... }: +let + opentofu = pkgs.khscodes.opentofu; +in +pkgs.writeShellApplication { + name = "instance-opentofu"; + runtimeInputs = [ + pkgs.uutils-coreutils-noprefix + opentofu + ]; + text = '' + fqdn="$1" + config="$2" + cmd="''${3:-apply}" + dir="$(mktemp -dt "$fqdn-pre-provisioning.XXXXXX")" + mkdir -p "$dir" + cat "''${config}" > "$dir/config.tf.json" + + tofu -chdir="$dir" init > /dev/null + tofu -chdir="$dir" "$cmd" + ''; +} diff --git a/nix/packages/opentofu/default.nix b/nix/packages/opentofu/default.nix index 074cc9c..2d18324 100644 --- a/nix/packages/opentofu/default.nix +++ b/nix/packages/opentofu/default.nix @@ -1 +1,7 @@ -{ pkgs }: pkgs.opentofu.withPlugins (p: [ pkgs.khscodes.terraform-provider-unifi ]) +{ pkgs }: +pkgs.opentofu.withPlugins (p: [ + pkgs.khscodes.terraform-provider-unifi + pkgs.khscodes.terraform-provider-cloudflare + pkgs.khscodes.terraform-provider-hcloud + pkgs.khscodes.terraform-provider-openstack +]) diff --git a/nix/packages/pre-provisioning/default.nix b/nix/packages/pre-provisioning/default.nix index 7d459da..bfb86a3 100644 --- a/nix/packages/pre-provisioning/default.nix +++ b/nix/packages/pre-provisioning/default.nix @@ -7,21 +7,27 @@ pkgs.writeShellApplication { runtimeInputs = [ pkgs.nix pkgs.khscodes.bw-opentofu + pkgs.khscodes.instance-opentofu + pkgs.khscodes.openbao-helper + pkgs.jq ]; # TODO: Use secret source and required secrets to set up the correct env variables text = '' hostname="$1" + cmd="''${2:-apply}" baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.provisioning' - config="$(nix eval --raw "''${baseAttr}.preConfig")" + config="$(nix build --no-link --print-out-paths "''${baseAttr}.preConfig")" secretsSource="$(nix eval --raw "''${baseAttr}.pre.secretsSource")" + endpoints="$(nix eval --json "''${baseAttr}.pre.endpoints")" if [[ "$config" == "null" ]]; then echo "No preprovisioning needed" exit 0 fi if [[ "$secretsSource" == "vault" ]]; then - >&2 echo "Provisioning using vault is not yet implemented" - exit 1 + readarray -t endpoints_args < <(echo "$endpoints" | jq -cr 'map(["-e", .])[][]') + openbao-helper wrap-program "''${endpoints_args[@]}" -- instance-opentofu "$hostname" "$config" "$cmd" + exit 0 fi - bw-opentofu "$hostname" "$config" "pre" + bw-opentofu "$hostname" "$config" "$cmd" ''; } diff --git a/nix/packages/terraform-provider-cloudflare/default.nix b/nix/packages/terraform-provider-cloudflare/default.nix new file mode 100644 index 0000000..92653dd --- /dev/null +++ b/nix/packages/terraform-provider-cloudflare/default.nix @@ -0,0 +1,10 @@ +{ pkgs }: +pkgs.terraform-providers.mkProvider { + hash = "sha256-rgXsROzfjtUw994JH8x+j/UNMyl7E9cZ+77Fczc3uB8="; + homepage = "https://registry.terraform.io/providers/cloudflare/cloudflare"; + owner = "cloudflare"; + repo = "terraform-provider-cloudflare"; + rev = "v4.52.0"; + spdx = "MPL-2.0"; + vendorHash = "sha256-RULgejA/RTDHhRJRiqlgckK4Ut3GLvIE081/i6gQTjI="; +} diff --git a/nix/packages/terraform-provider-hcloud/default.nix b/nix/packages/terraform-provider-hcloud/default.nix new file mode 100644 index 0000000..befe4b5 --- /dev/null +++ b/nix/packages/terraform-provider-hcloud/default.nix @@ -0,0 +1,10 @@ +{ pkgs }: +pkgs.terraform-providers.mkProvider { + hash = "sha256-/BcK9K/jNEU4r7mFc4rrhNnPlyH98UrDUCgIluY52fA="; + homepage = "https://registry.terraform.io/providers/hetznercloud/hcloud"; + owner = "hetznercloud"; + repo = "terraform-provider-hcloud"; + rev = "v1.51.0"; + spdx = "MPL-2.0"; + vendorHash = "sha256-jbNkhNSSO9jT20J6dVhBEbN9cwtNrvx5EUcyOZcMd4Y="; +} diff --git a/nix/packages/terraform-provider-openstack/default.nix b/nix/packages/terraform-provider-openstack/default.nix new file mode 100644 index 0000000..2e2b3ee --- /dev/null +++ b/nix/packages/terraform-provider-openstack/default.nix @@ -0,0 +1,10 @@ +{ pkgs }: +pkgs.terraform-providers.mkProvider { + hash = "sha256-pGNHWhg/1LM1IJYEVLppCJWVzow+j3WPW+H8yWQXMyM="; + homepage = "https://registry.terraform.io/providers/terraform-provider-openstack/openstack"; + owner = "terraform-provider-openstack"; + repo = "terraform-provider-openstack"; + rev = "v3.2.0"; + spdx = "MPL-2.0"; + vendorHash = "sha256-mTWLix4A0GRe7ayHTwU3Jt+DfDKMIKJlt1I6JuL3wXU="; +} diff --git a/nix/profiles/hetzner-server.nix b/nix/profiles/hetzner-server.nix index a0612e8..4bd88cd 100644 --- a/nix/profiles/hetzner-server.nix +++ b/nix/profiles/hetzner-server.nix @@ -1,5 +1,6 @@ -{ ... }: +{ modulesPath, ... }: { + imports = [ "${modulesPath}/profiles/qemu-guest.nix" ]; config.khscodes = { hetzner.enable = true; sshd.enable = true; diff --git a/nix/profiles/khs-openstack-server.nix b/nix/profiles/khs-openstack-server.nix new file mode 100644 index 0000000..5511ede --- /dev/null +++ b/nix/profiles/khs-openstack-server.nix @@ -0,0 +1,7 @@ +{ modulesPath, ... }: +{ + imports = [ "${modulesPath}/profiles/qemu-guest.nix" ]; + config.khscodes = { + sshd.enable = true; + }; +} diff --git a/nix/systems/x86_64-linux/test.kaareskovgaard.net/default.nix b/nix/systems/x86_64-linux/test.kaareskovgaard.net/default.nix new file mode 100644 index 0000000..93216be --- /dev/null +++ b/nix/systems/x86_64-linux/test.kaareskovgaard.net/default.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: +{ + imports = [ "${inputs.self}/nix/profiles/khs-openstack-server.nix" ]; + khscodes.khs-openstack-instance = { + enable = true; + flavor = "m.medium"; + secretsSource = "vault"; + }; + khscodes.fqdn = "test.kaareskovgaard.net"; + system.stateVersion = "25.05"; +}