devops/quadlets/modules/quadlet-app/main.tf

220 lines
6.0 KiB
HCL

variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
description = "Target server IP"
type = string
}
variable "ssh_private_key_path" {
description = "Path to SSH private key"
type = string
default = "~/.ssh/id_rsa"
}
variable "app_name" {
description = "Name of the application"
type = string
}
variable "image" {
description = "Container image"
type = string
}
variable "ports" {
description = "List of port mappings (e.g., ['8080:80', '8443:443'])"
type = list(string)
default = []
}
variable "volumes" {
description = "List of volume mounts (e.g., ['/host/path:/container/path:Z'])"
type = list(string)
default = []
}
variable "environment" {
description = "Environment variables as key-value pairs"
type = map(string)
default = {}
}
variable "command" {
description = "Command to run in container (list of strings)"
type = list(string)
default = []
}
variable "haproxy_services" {
description = "Multiple HAProxy service configurations"
type = list(object({
name = string
domain = string
port = string
host = optional(string, "127.0.0.1")
tls = optional(bool, false)
}))
default = []
}
variable "depends_on_services" {
description = "List of systemd services this app depends on"
type = list(string)
default = []
}
variable "restart_policy" {
description = "Systemd restart policy"
type = string
default = "always"
}
variable "healthcmd" {
default = ""
}
locals {
# Build all HAProxy labels for multiple services
haproxy_labels = flatten([
for svc in var.haproxy_services : [
"Label=haproxy.${svc.name}.enable=true",
"Label=haproxy.${svc.name}.domain=${svc.domain}",
"Label=haproxy.${svc.name}.port=${svc.port}",
"Label=haproxy.${svc.name}.host=${svc.host}",
"Label=haproxy.${svc.name}.tls=${svc.tls}"
]
])
}
resource "null_resource" "deploy_quadlet_app" {
depends_on = [var.wait_on]
triggers = {
app_name = var.app_name
image = var.image
server_ip = var.server_ip
ports = jsonencode(var.ports)
volumes = jsonencode(var.volumes)
environment = jsonencode(var.environment)
command = jsonencode(var.command)
haproxy_services = jsonencode(var.haproxy_services)
depends_on_services = jsonencode(var.depends_on_services)
ssh_private_key_path = var.ssh_private_key_path
restart_policy = var.restart_policy
}
provisioner "remote-exec" {
inline = compact(flatten([
[
# Wait for cloud-init to complete before proceeding
"cloud-init status --wait || true",
# Verify the user systemd session is ready and linger is enabled
"timeout 60 bash -c 'until loginctl show-user fourlights | grep -q \"Linger=yes\"; do sleep 2; done'",
# Create base quadlet file
"cat > /tmp/${var.app_name}.container << 'EOF'",
"[Unit]",
"Description=${var.app_name} Service",
"After=network-online.target",
"",
"[Container]",
"Image=${var.image}",
],
# Add ports (only if not empty)
length(var.ports) > 0 ? formatlist("PublishPort=127.0.0.1:%s", var.ports) : [],
# Add volumes (only if not empty)
length(var.volumes) > 0 ? formatlist("Volume=%s", var.volumes) : [],
# Add environment variables (only if not empty)
length(var.environment) > 0 ? formatlist("Environment=%s=%s", keys(var.environment), values(var.environment)) : [],
# Add command (only if not empty)
length(var.command) > 0 ? ["Exec=${join(" ", var.command)}"] : [],
# Add pre-computed HAProxy labels (only if not empty)
length(local.haproxy_labels) > 0 ? local.haproxy_labels : [],
# Add health checks if not empty
var.healthcmd != "" ? ["HealthCmd=${var.healthcmd}"] : [],
[
"",
"[Service]",
"Restart=${var.restart_policy}",
"",
"[Install]",
"WantedBy=default.target",
"EOF",
# Create volume directory
"mkdir -p /opt/storage/data/${var.app_name}",
# Move and activate
# Create directory more robustly
"test -d ~/.config/containers/systemd || mkdir -p ~/.config/containers/systemd",
"cp /tmp/${var.app_name}.container ~/.config/containers/systemd/${var.app_name}.container",
"systemctl --user daemon-reload",
"timeout 60 bash -c 'until systemctl --user list-unit-files | grep -q \"^${var.app_name}.service\"; do sleep 2; systemctl --user daemon-reload; done'",
"systemctl --user start ${var.app_name}",
"systemctl --user status ${var.app_name} --no-pager",
]
]))
connection {
type = "ssh"
host = var.server_ip
user = "fourlights"
agent = true
agent_identity = var.ssh_private_key_path
}
}
provisioner "remote-exec" {
when = destroy
inline = [
# Stop and remove the service
"systemctl --user stop ${self.triggers.app_name} || true",
# Remove the .container file
"rm -f ~/.config/containers/systemd/${self.triggers.app_name}.container",
# Reload systemd to remove the generated service
"systemctl --user daemon-reload",
# Force remove any lingering containers
"podman rm -f ${self.triggers.app_name} || true"
]
connection {
type = "ssh"
host = self.triggers.server_ip
user = "fourlights"
agent = true
agent_identity = self.triggers.ssh_private_key_path
}
}
}
output "app_name" {
value = var.app_name
}
output "service_status" {
value = "${var.app_name} deployed"
}
output "app_urls" {
value = [for svc in var.haproxy_services : format("%s://%s", (svc.tls == true ? "https" : "http"), svc.domain)]
}
output "installed" {
value = true
depends_on = [null_resource.deploy_quadlet_app]
}