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] }