Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions modules/bootstrap/examples/basic/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ provider "harvester" {
provider "tls" {}

module "bootstrap" {
source = "github.com/wso2-enterprise/open-cloud-datacenter//modules/bootstrap?ref=v0.1.0"
source = "../.."

vm_name = "rancher-bootstrap"
node_count = 1
Expand All @@ -47,7 +47,7 @@ module "bootstrap" {

# Credentials – supply via tfvars or environment variables; never hardcode
vm_password = var.vm_password
rancher_admin_password = var.rancher_admin_password
bootstrap_password = var.bootstrap_password

rancher_hostname = "rancher.example.internal"

Expand All @@ -63,7 +63,7 @@ variable "vm_password" {
sensitive = true
}

variable "rancher_admin_password" {
variable "bootstrap_password" {
type = string
sensitive = true
}
Expand Down
132 changes: 87 additions & 45 deletions modules/bootstrap/main.tf
Original file line number Diff line number Diff line change
@@ -1,107 +1,157 @@
terraform {
required_version = ">= 1.3"
required_version = ">= 1.5"
required_providers {
harvester = {
source = "harvester/harvester"
version = "~> 1.7"
}
rancher2 = {
source = "rancher/rancher2"
version = "~> 13.1"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
}
}

# ── SSH key pair (greenfield only) ────────────────────────────────────────────
# Set create_ssh_key = false to attach existing ssh_key_ids instead.
resource "tls_private_key" "bootstrap_key" {
count = var.create_ssh_key ? 1 : 0
algorithm = "RSA"
rsa_bits = 4096
}

resource "harvester_ssh_key" "bootstrap_key" {
count = var.create_ssh_key ? 1 : 0
name = "${var.vm_name}-ssh-key"
namespace = var.harvester_namespace
public_key = tls_private_key.bootstrap_key.public_key_openssh
public_key = tls_private_key.bootstrap_key[0].public_key_openssh
}

# Removed dynamic VLAN creation as DHCP is failing in the cluster

# 1. Create the Cloud-Init Secret for the VM (Bypasses 2KB limit)
# ── Cloud-init secret (greenfield only) ──────────────────────────────────────
# Set create_cloudinit_secret = false and provide existing_cloudinit_secret_name instead.
resource "harvester_cloudinit_secret" "cloudinit" {
count = var.node_count
count = var.create_cloudinit_secret ? var.node_count : 0
name = var.node_count > 1 ? "${var.vm_name}-cloudinit-${count.index}" : "${var.vm_name}-cloudinit"
namespace = var.harvester_namespace

user_data = templatefile("${path.module}/templates/cloud-init.yaml.tpl", {
password = var.vm_password,
cluster_dns = var.rancher_hostname,
rancher_password = var.bootstrap_password,
ssh_public_key = tls_private_key.bootstrap_key.public_key_openssh,
node_index = count.index,
node_count = var.node_count,
lb_ip = var.ippool_start # Using the LB IP for join logic
password = var.vm_password
cluster_dns = var.rancher_hostname
rancher_password = var.bootstrap_password
ssh_public_key = tls_private_key.bootstrap_key[0].public_key_openssh
node_index = count.index
node_count = var.node_count
lb_ip = var.ippool_start
})
}

# 2. Create the Harvester VM
locals {
# Resolve the SSH key IDs: either freshly generated or caller-supplied
ssh_key_ids = var.create_ssh_key ? [harvester_ssh_key.bootstrap_key[0].id] : var.ssh_key_ids
}

# ── Input validation ──────────────────────────────────────────────────────────

# The cloud-init template embeds the generated SSH public key. If create_ssh_key
# is false the tls_private_key resource is empty, causing an invalid-index error.
check "ssh_key_required_for_cloudinit" {
assert {
condition = !var.create_cloudinit_secret || var.create_ssh_key
error_message = "create_ssh_key must be true when create_cloudinit_secret is true (the cloud-init template embeds the generated SSH public key)."
}
}

# When reusing an existing cloud-init secret, the name must be provided.
check "existing_cloudinit_secret_name_required" {
assert {
condition = var.create_cloudinit_secret || var.existing_cloudinit_secret_name != ""
error_message = "existing_cloudinit_secret_name is required when create_cloudinit_secret = false."
}
}

# ── Rancher server VM ─────────────────────────────────────────────────────────
resource "harvester_virtualmachine" "rancher_server" {
count = var.node_count
name = var.node_count > 1 ? "${var.vm_name}-${count.index}" : var.vm_name
namespace = var.harvester_namespace
restart_after_update = true

cpu = 4
memory = "8Gi"
cpu = var.vm_cpu
memory = var.vm_memory

run_strategy = "RerunOnFailure"
machine_type = "q35"

ssh_keys = [harvester_ssh_key.bootstrap_key.id]
ssh_keys = local.ssh_key_ids

network_interface {
name = "nic-1"
type = "masquerade"
# Masquerade (NAT): default for greenfield; no external network required
dynamic "network_interface" {
for_each = var.network_type == "masquerade" ? [1] : []
content {
name = var.network_interface_name
type = "masquerade"
}
}

# Bridge: for VMs that need direct VLAN access (e.g. existing production VMs)
dynamic "network_interface" {
for_each = var.network_type == "bridge" ? [1] : []
content {
name = var.network_interface_name
type = "bridge"
network_name = var.network_name
mac_address = var.network_mac_address != "" ? var.network_mac_address : null
}
}

disk {
name = "rootdisk"
name = var.vm_disk_name
type = "disk"
size = "40Gi"
size = var.vm_disk_size
bus = "virtio"
boot_order = 1

image = var.ubuntu_image_id
auto_delete = true
auto_delete = var.vm_disk_auto_delete
}

# USB tablet input device — some VMs require this for correct cursor behaviour
# in the Harvester console; set enable_usb_tablet = true to include it.
dynamic "input" {
for_each = var.enable_usb_tablet ? [1] : []
content {
name = "tablet"
type = "tablet"
bus = "usb"
}
}

cloudinit {
user_data_secret_name = harvester_cloudinit_secret.cloudinit[count.index].name
network_data = ""
user_data_secret_name = var.create_cloudinit_secret ? harvester_cloudinit_secret.cloudinit[count.index].name : var.existing_cloudinit_secret_name
network_data_secret_name = var.create_cloudinit_secret ? null : var.existing_cloudinit_secret_name
}

# Rancher is installed entirely by cloud-init inside the VM (RKE2 + cert-manager + Helm).
# The VM uses a masquerade network so Terraform cannot SSH into it directly.
provisioner "local-exec" {
command = "echo 'Please wait for cloud-init to finish installing RKE2/K3s and Rancher internally!'"
command = var.create_cloudinit_secret ? "echo 'VM created — cloud-init will install RKE2/Rancher internally.'" : "echo 'VM imported — cloud-init ran at initial provision time.'"
}
}

# 3. Expose the Rancher VM via a Load Balancer
# ── Load Balancer + IP Pool (greenfield only) ─────────────────────────────────
# Set create_lb = false when the Rancher VM is reachable directly via its
# bridge IP (no dedicated LB/IP-pool needed).
resource "harvester_loadbalancer" "rancher_lb" {
count = var.create_lb ? 1 : 0
name = "${var.vm_name}-lb"
namespace = var.harvester_namespace

depends_on = [
harvester_virtualmachine.rancher_server,
harvester_ippool.rancher_ips
harvester_ippool.rancher_ips,
]

workload_type = "vm"
ipam = "pool"
ippool = harvester_ippool.rancher_ips.name
ippool = harvester_ippool.rancher_ips[0].name

listener {
name = "https"
Expand Down Expand Up @@ -131,9 +181,9 @@ resource "harvester_loadbalancer" "rancher_lb" {
}
}

# 4. Create an IP Pool for the Load Balancer
resource "harvester_ippool" "rancher_ips" {
name = "${var.vm_name}-ips"
count = var.create_lb ? 1 : 0
name = "${var.vm_name}-ips"

range {
start = var.ippool_start
Expand All @@ -142,11 +192,3 @@ resource "harvester_ippool" "rancher_ips" {
gateway = var.ippool_gateway
}
}

# Rancher is installed inside the VM by cloud-init (cert-manager + Helm).
# rancher2_bootstrap waits for Rancher to be reachable and sets the permanent admin
# password. Re-run `terraform apply` if Rancher is still starting up on first attempt.
resource "rancher2_bootstrap" "admin" {
initial_password = var.bootstrap_password
password = var.rancher_admin_password
}
17 changes: 10 additions & 7 deletions modules/bootstrap/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
locals {
rancher_lb_ip = var.create_lb ? harvester_loadbalancer.rancher_lb[0].ip_address : var.static_rancher_ip
}

output "rancher_hostname" {
value = var.rancher_hostname
description = "The FQDN of the bootstrapped Rancher server"
description = "FQDN of the Rancher server"
}

output "rancher_lb_ip" {
value = harvester_loadbalancer.rancher_lb.ip_address
description = "The IP address of the LoadBalancer exposing Rancher"
value = local.rancher_lb_ip
description = "IP used to reach Rancher: LoadBalancer IP (greenfield) or bridge VM IP (brownfield)"
}

output "admin_token" {
value = rancher2_bootstrap.admin.token
description = "Rancher admin API token for use by downstream phases"
sensitive = true
output "vm_id" {
value = harvester_virtualmachine.rancher_server[0].id
description = "Harvester resource ID of the Rancher server VM (namespace/name)"
}
Loading
Loading