Files
AZ-NIX-SERVERS/hosts/AZ-PRM-1/services/containers/semaphore.nix
T

92 lines
3.7 KiB
Nix

{
config,
lib,
...
}: let
serviceName = "semaphore";
servicePort = config.m3ta.ports.get serviceName;
in {
virtualisation.oci-containers.containers."${serviceName}" = {
# Image contains the Semaphore server plus ansible, terraform,
# opentofu and terragrunt runners.
image = "docker.io/semaphoreui/semaphore:latest";
environment = {
SEMAPHORE_DB_DIALECT = "postgres";
SEMAPHORE_DB_HOST = "postgres";
SEMAPHORE_DB_PORT = "5432";
SEMAPHORE_PLAYBOOK_PATH = "/tmp/semaphore/";
};
# SEMAPHORE_DB (name), SEMAPHORE_DB_USER, SEMAPHORE_DB_PASS and the
# admin bootstrap vars (SEMAPHORE_ADMIN*) come from the agenix secret
# (podman env-file).
environmentFiles = [config.age.secrets.semaphore-env.path];
ports = ["127.0.0.1:${toString servicePort}:3000"];
volumes = [
"semaphore_data:/var/lib/semaphore"
];
# Public DNS: the corporate DNS behind aardvark answers AAAA queries for
# galaxy.ansible.com but not A queries — with no IPv6 route in the web
# network ansible-galaxy downloads failed (EAI_AGAIN / empty responses).
# Bypass with public resolvers; git.az-gruppe.com is publicly resolvable,
# targets connect by IP (same pattern as librechat/homarr on AZ-CLD-1).
extraOptions = ["--network=web" "--ip=10.89.0.17" "--add-host=postgres:10.89.0.1" "--dns=8.8.8.8" "--dns=8.8.4.4"];
};
# Idempotent provisioning of the semaphore role/database on the host
# PostgreSQL 17 (same pattern as atrocore-db-init: initialScript cannot
# be used, the cluster is already initialized and the password must
# never land in the Nix store).
systemd.services.semaphore-db-init = {
description = "Provision semaphore role/database from agenix secret";
after = ["postgresql.service"];
before = ["podman-${serviceName}.service"];
wants = ["postgresql.service"];
wantedBy = ["multi-user.target"];
serviceConfig = {
Type = "oneshot";
User = "postgres";
Group = "postgres";
RemainAfterExit = true;
};
path = [config.services.postgresql.package];
# Env-file format: plain KEY=value lines (no quotes), same file the
# container consumes.
script = ''
set -euo pipefail
ENV_FILE="${config.age.secrets.semaphore-env.path}"
get_val() {
grep -m1 "^$1=" "$ENV_FILE" | head -n1 | cut -d= -f2- | tr -d '\r'
}
db_name="$(get_val SEMAPHORE_DB)"
db_user="$(get_val SEMAPHORE_DB_USER)"
db_pass="$(get_val SEMAPHORE_DB_PASS)"
if [ -z "$db_name" ] || [ -z "$db_user" ] || [ -z "$db_pass" ]; then
echo "semaphore-db-init: SEMAPHORE_DB/SEMAPHORE_DB_USER/SEMAPHORE_DB_PASS missing in $ENV_FILE" >&2
exit 1
fi
psql -v ON_ERROR_STOP=1 -d postgres \
-v db_name="$db_name" -v db_user="$db_user" -v db_pass="$db_pass" <<'SQL'
SELECT format('CREATE ROLE %I LOGIN', :'db_user')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'db_user') \gexec
ALTER ROLE :"db_user" WITH LOGIN PASSWORD :'db_pass';
SELECT format('CREATE DATABASE %I OWNER %I', :'db_name', :'db_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'db_name') \gexec
ALTER DATABASE :"db_name" OWNER TO :"db_user";
SQL
echo "semaphore-db-init: role/database '$db_name' ready"
'';
};
# Traefik configuration specific to semaphore
services.traefik.dynamicConfigOptions.http = {
services.${serviceName}.loadBalancer.servers = [{url = "http://localhost:${toString servicePort}/";}];
routers.${serviceName} = {
rule = "Host(`sem.l.az-gruppe.com`)";
tls = {certResolver = "ionos";};
service = serviceName;
entrypoints = "websecure";
};
};
}