From 6fcdd1262d894129e1ec1044cc87547ca6fd8d9d Mon Sep 17 00:00:00 2001 From: Leopere Date: Tue, 3 Mar 2026 12:51:12 -0500 Subject: [PATCH] Bake git commit into Go binary for version traceability - Add internal/version package with ldflags-injected Commit/BuildTime - Dockerfile accepts BUILD_COMMIT/BUILD_TIME args, passes via -ldflags - Log version on startup, expose GET /version endpoint - Show commit hash badge in bottom-right of landing + dashboard pages - Deploy scripts gate on clean git tree and pass commit to build - Remove staging files, misc config updates Made-with: Cursor --- .cursorignore | 5 + .woodpecker.yml | 103 -------------- docker-compose.dev.yml | 4 + docker-compose.staging.yml | 18 --- docker-compose.swarm-dev.yml | 14 ++ docker/authelia/Dockerfile.production | 12 +- docker/authelia/config/configuration.acl.yml | 6 + docker/authelia/config/configuration.ldap.yml | 2 +- .../authelia/config/configuration.server.yml | 4 +- docker/mariadb/Dockerfile.production | 2 +- docker/redis/Dockerfile.production | 4 +- docker/ss-atlas/Dockerfile | 9 +- docker/ss-atlas/cmd/main.go | 2 + docker/ss-atlas/internal/config/config.go | 2 + .../ss-atlas/internal/config/config_test.go | 5 +- docker/ss-atlas/internal/handlers/activate.go | 2 +- .../ss-atlas/internal/handlers/dashboard.go | 4 + docker/ss-atlas/internal/handlers/routes.go | 9 ++ .../internal/handlers/subscription.go | 6 +- docker/ss-atlas/internal/version/version.go | 10 ++ .../ss-atlas/templates/pages/dashboard.html | 12 ++ docker/ss-atlas/templates/pages/landing.html | 12 ++ scripts/deploy-stack-dev.sh | 38 +++++ scripts/local-dns-setup.sh | 22 +++ scripts/local-rebuild-deploy.sh | 78 +++++++++++ stack.production.yml | 44 +++--- stack.staging.yml | 130 ------------------ 27 files changed, 280 insertions(+), 279 deletions(-) create mode 100644 .cursorignore delete mode 100644 docker-compose.staging.yml create mode 100644 docker-compose.swarm-dev.yml create mode 100644 docker/ss-atlas/internal/version/version.go create mode 100755 scripts/deploy-stack-dev.sh create mode 100755 scripts/local-dns-setup.sh create mode 100755 scripts/local-rebuild-deploy.sh delete mode 100644 stack.staging.yml diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..2c8af5a --- /dev/null +++ b/.cursorignore @@ -0,0 +1,5 @@ +# Authelia – stable/done; keep out of context for ss-atlas and other work +docker/authelia/ +authelia-dev-config.yml +docker/mariadb/ +docker/redis/ \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml index d8e6df1..d4d8df6 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -19,109 +19,6 @@ steps: when: event: push - # Build and Push for Staging - build-push-staging: - name: build-push-staging - image: woodpeckerci/plugin-docker-buildx - environment: - REGISTRY_USER: - from_secret: REGISTRY_USER - REGISTRY_PASSWORD: - from_secret: REGISTRY_PASSWORD - DOCKER_REGISTRY_USER: - from_secret: DOCKER_REGISTRY_USER - DOCKER_REGISTRY_PASSWORD: - from_secret: DOCKER_REGISTRY_PASSWORD - # Authelia Core Secrets - AUTHENTICATION_BACKEND_LDAP_PASSWORD: - from_secret: AUTHENTICATION_BACKEND_LDAP_PASSWORD - IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: - from_secret: IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET - STORAGE_ENCRYPTION_KEY: - from_secret: STORAGE_ENCRYPTION_KEY - SESSION_SECRET: - from_secret: SESSION_SECRET - NOTIFIER_SMTP_PASSWORD: - from_secret: NOTIFIER_SMTP_PASSWORD - # OIDC Secrets - IDENTITY_PROVIDERS_OIDC_HMAC_SECRET: - from_secret: IDENTITY_PROVIDERS_OIDC_HMAC_SECRET - IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY: - from_secret: IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY - IDENTITY_PROVIDERS_OIDC_JWKS_KEY: - from_secret: IDENTITY_PROVIDERS_OIDC_JWKS_KEY - # OAuth Client Secrets removed - handled by Docker Swarm secrets at runtime - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - - echo "Logging into registries" - - echo "$${DOCKER_REGISTRY_PASSWORD}" | docker login -u "$${DOCKER_REGISTRY_USER}" --password-stdin - - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us - - echo "Building and pushing application for staging" - - docker compose -f docker-compose.staging.yml build --no-cache - - docker compose -f docker-compose.staging.yml push - when: - branch: main - event: push - - # Deploy Staging - deploy-staging: - name: deploy-staging - image: woodpeckerci/plugin-docker-buildx - environment: - REGISTRY_USER: - from_secret: REGISTRY_USER - REGISTRY_PASSWORD: - from_secret: REGISTRY_PASSWORD - # Authelia Core Secrets - AUTHENTICATION_BACKEND_LDAP_PASSWORD: - from_secret: AUTHENTICATION_BACKEND_LDAP_PASSWORD - IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: - from_secret: IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET - STORAGE_ENCRYPTION_KEY: - from_secret: STORAGE_ENCRYPTION_KEY - SESSION_SECRET: - from_secret: SESSION_SECRET - NOTIFIER_SMTP_PASSWORD: - from_secret: NOTIFIER_SMTP_PASSWORD - # OIDC Secrets - IDENTITY_PROVIDERS_OIDC_HMAC_SECRET: - from_secret: IDENTITY_PROVIDERS_OIDC_HMAC_SECRET - IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY: - from_secret: IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY - IDENTITY_PROVIDERS_OIDC_JWKS_KEY: - from_secret: IDENTITY_PROVIDERS_OIDC_JWKS_KEY - # OAuth Client Secrets removed - handled by Docker Swarm secrets at runtime - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - - echo "Deploying to staging environment" - - echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us - - docker stack deploy --with-registry-auth -c ./stack.staging.yml $${CI_REPO_NAME}-staging - when: - branch: main - event: push - - # Cleanup Staging - cleanup-staging: - name: cleanup-staging - image: woodpeckerci/plugin-docker-buildx - environment: - REGISTRY_USER: - from_secret: REGISTRY_USER - REGISTRY_PASSWORD: - from_secret: REGISTRY_PASSWORD - volumes: - - /var/run/docker.sock:/var/run/docker.sock - commands: - - echo "Cleaning up staging environment" - - for i in {1..5}; do docker stack rm ${CI_REPO_NAME}-staging && break || sleep 10; done - - docker compose -f docker-compose.staging.yml down - - docker compose -f docker-compose.staging.yml rm -f - when: - branch: main - event: push - # Build and Push for Production build-push-production: name: build-push-production diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 4463337..87ebfaf 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -197,6 +197,9 @@ services: build: context: ./docker/ss-atlas/ dockerfile: Dockerfile + args: + BUILD_COMMIT: ${BUILD_COMMIT:-unknown} + BUILD_TIME: ${BUILD_TIME:-unknown} container_name: atlas_ss_app environment: - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-sk_test_placeholder} @@ -211,6 +214,7 @@ services: - AUTHELIA_URL=http://login.bc.a250.ca - TRAEFIK_DOMAIN=bc.a250.ca - TRAEFIK_NETWORK=authelia_dev + - CUSTOMER_DOMAIN=app.a250.ca - TEMPLATE_PATH=/app/templates volumes: - /var/run/docker.sock:/var/run/docker.sock:ro diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml deleted file mode 100644 index 40ba270..0000000 --- a/docker-compose.staging.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '3.8' - -services: - mariadb: - build: - context: ./docker/mariadb/ - dockerfile: Dockerfile - image: git.nixc.us/a250/authelia:staging-mariadb - redis: - build: - context: ./docker/redis/ - dockerfile: Dockerfile - image: git.nixc.us/a250/authelia:staging-redis - authelia: - build: - context: ./docker/authelia/ - dockerfile: Dockerfile - image: git.nixc.us/a250/authelia:staging-authelia \ No newline at end of file diff --git a/docker-compose.swarm-dev.yml b/docker-compose.swarm-dev.yml new file mode 100644 index 0000000..e8b8f3e --- /dev/null +++ b/docker-compose.swarm-dev.yml @@ -0,0 +1,14 @@ +# Override for local swarm: overlay network so customer stacks attach to Traefik. +# Traefik stays in container mode (swarmMode=false) so it sees both compose and stack containers. +# +# Usage: ./scripts/deploy-stack-dev.sh +# Customer stacks get clientname.app.a250.ca. Run scripts/local-dns-setup.sh for DNS. + +services: + ss-atlas: + environment: + - CUSTOMER_DOMAIN=app.a250.ca + +networks: + authelia_dev: + external: true diff --git a/docker/authelia/Dockerfile.production b/docker/authelia/Dockerfile.production index 3f35fdc..870bc6a 100644 --- a/docker/authelia/Dockerfile.production +++ b/docker/authelia/Dockerfile.production @@ -1 +1,11 @@ -FROM git.nixc.us/a250/authelia:staging-authelia \ No newline at end of file +FROM authelia/authelia:4 + +COPY config/ /config/ + +RUN mkdir -p /config/assets + +CMD ["authelia", \ + "--config=/config/configuration.server.yml", \ + "--config=/config/configuration.ldap.yml", \ + "--config=/config/configuration.acl.yml", \ + "--config=/config/configuration.notifier.yml"] diff --git a/docker/authelia/config/configuration.acl.yml b/docker/authelia/config/configuration.acl.yml index 3fd36ef..6c46372 100644 --- a/docker/authelia/config/configuration.acl.yml +++ b/docker/authelia/config/configuration.acl.yml @@ -32,6 +32,12 @@ access_control: - "group:customers" policy: one_factor + # Customer demo subdomains (e.g. clientname.app.a250.ca) + - domain_regex: '^[a-z0-9-]+\.app\.a250\.ca$' + subject: + - "group:customers" + policy: one_factor + # ss-atlas app public routes (landing, webhook) - domain: 'app.{{ env "TRAEFIK_DOMAIN" }}' policy: bypass diff --git a/docker/authelia/config/configuration.ldap.yml b/docker/authelia/config/configuration.ldap.yml index 25b569a..f96c5d5 100644 --- a/docker/authelia/config/configuration.ldap.yml +++ b/docker/authelia/config/configuration.ldap.yml @@ -4,7 +4,7 @@ authentication_backend: refresh_interval: 5m ldap: implementation: custom - address: ldap://lldap_lldap:3890 + address: ldap://lldap:3890 timeout: 5s start_tls: false tls: diff --git a/docker/authelia/config/configuration.server.yml b/docker/authelia/config/configuration.server.yml index 697137b..e5e3ad1 100644 --- a/docker/authelia/config/configuration.server.yml +++ b/docker/authelia/config/configuration.server.yml @@ -42,7 +42,7 @@ storage: # local: # path: /config/db.sqlite3 mysql: - address: 'tcp://authelia_mariadb:3306' + address: 'tcp://mariadb:3306' database: authelia username: authelia ## Password can also be set using a secret: https://www.authelia.com/docs/configuration/secrets.html @@ -66,7 +66,7 @@ session: remember_me: '1d' redis: - host: 'authelia_redis' + host: 'redis' port: 6379 database_index: 0 maximum_active_connections: 8 diff --git a/docker/mariadb/Dockerfile.production b/docker/mariadb/Dockerfile.production index d3097b2..3775d47 100644 --- a/docker/mariadb/Dockerfile.production +++ b/docker/mariadb/Dockerfile.production @@ -1 +1 @@ -FROM git.nixc.us/a250/authelia:staging-mariadb \ No newline at end of file +FROM mariadb:latest diff --git a/docker/redis/Dockerfile.production b/docker/redis/Dockerfile.production index 584b5f0..d0d2d47 100644 --- a/docker/redis/Dockerfile.production +++ b/docker/redis/Dockerfile.production @@ -1 +1,3 @@ -FROM git.nixc.us/a250/authelia:staging-redis \ No newline at end of file +FROM redis:latest + +CMD ["redis-server", "--appendonly", "yes"] diff --git a/docker/ss-atlas/Dockerfile b/docker/ss-atlas/Dockerfile index 2476831..ffd6658 100644 --- a/docker/ss-atlas/Dockerfile +++ b/docker/ss-atlas/Dockerfile @@ -1,14 +1,19 @@ FROM golang:1.23-alpine AS builder +ARG BUILD_COMMIT=unknown +ARG BUILD_TIME=unknown + WORKDIR /build COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -o /ss-atlas ./cmd/ +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags "-X git.nixc.us/a250/ss-atlas/internal/version.Commit=${BUILD_COMMIT} -X git.nixc.us/a250/ss-atlas/internal/version.BuildTime=${BUILD_TIME}" \ + -o /ss-atlas ./cmd/ FROM alpine:3.21 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates docker-cli WORKDIR /app COPY --from=builder /ss-atlas /app/ss-atlas COPY templates/ /app/templates/ diff --git a/docker/ss-atlas/cmd/main.go b/docker/ss-atlas/cmd/main.go index 78c905b..de12d29 100644 --- a/docker/ss-atlas/cmd/main.go +++ b/docker/ss-atlas/cmd/main.go @@ -14,9 +14,11 @@ import ( "git.nixc.us/a250/ss-atlas/internal/ldap" ssstripe "git.nixc.us/a250/ss-atlas/internal/stripe" "git.nixc.us/a250/ss-atlas/internal/swarm" + "git.nixc.us/a250/ss-atlas/internal/version" ) func main() { + log.Printf("ss-atlas %s", version.String()) cfg := config.Load() stripeClient := ssstripe.New(cfg) diff --git a/docker/ss-atlas/internal/config/config.go b/docker/ss-atlas/internal/config/config.go index c5e6a27..37aea10 100644 --- a/docker/ss-atlas/internal/config/config.go +++ b/docker/ss-atlas/internal/config/config.go @@ -17,6 +17,7 @@ type Config struct { TraefikDomain string TraefikNetwork string TemplatePath string + CustomerDomain string // e.g. app.a250.ca for clientname.app.a250.ca } func Load() *Config { @@ -35,6 +36,7 @@ func Load() *Config { TraefikDomain: envOrDefault("TRAEFIK_DOMAIN", "bc.a250.ca"), TraefikNetwork: envOrDefault("TRAEFIK_NETWORK", "authelia_dev"), TemplatePath: envOrDefault("TEMPLATE_PATH", "/app/templates"), + CustomerDomain: envOrDefault("CUSTOMER_DOMAIN", "app.a250.ca"), } } diff --git a/docker/ss-atlas/internal/config/config_test.go b/docker/ss-atlas/internal/config/config_test.go index b87993b..787387a 100644 --- a/docker/ss-atlas/internal/config/config_test.go +++ b/docker/ss-atlas/internal/config/config_test.go @@ -29,7 +29,7 @@ func TestLoadDefaults(t *testing.T) { "PORT", "APP_URL", "AUTHELIA_URL", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET", "STRIPE_PRICE_ID", "LLDAP_URL", "LLDAP_ADMIN_DN", "LLDAP_ADMIN_PASSWORD", "LLDAP_BASE_DN", - "DOCKER_HOST", "TRAEFIK_DOMAIN", "TRAEFIK_NETWORK", "TEMPLATE_PATH", + "DOCKER_HOST", "TRAEFIK_DOMAIN", "TRAEFIK_NETWORK", "TEMPLATE_PATH", "CUSTOMER_DOMAIN", } for _, k := range envKeys { os.Unsetenv(k) @@ -54,9 +54,10 @@ func TestLoadDefaults(t *testing.T) { {"LDAPAdminDN", cfg.LDAPAdminDN, "uid=admin,ou=people,dc=a250,dc=ca"}, {"LDAPBaseDN", cfg.LDAPBaseDN, "dc=a250,dc=ca"}, {"DockerHost", cfg.DockerHost, "unix:///var/run/docker.sock"}, - {"TraefikDomain", cfg.TraefikDomain, "bc.a250.ca"}, + { "TraefikDomain", cfg.TraefikDomain, "bc.a250.ca"}, {"TraefikNetwork", cfg.TraefikNetwork, "authelia_dev"}, {"TemplatePath", cfg.TemplatePath, "/app/templates"}, + {"CustomerDomain", cfg.CustomerDomain, "app.a250.ca"}, } for _, tt := range tests { if tt.got != tt.want { diff --git a/docker/ss-atlas/internal/handlers/activate.go b/docker/ss-atlas/internal/handlers/activate.go index 6b5f8e3..13d2d6d 100644 --- a/docker/ss-atlas/internal/handlers/activate.go +++ b/docker/ss-atlas/internal/handlers/activate.go @@ -55,7 +55,7 @@ func (a *App) handleActivatePost(w http.ResponseWriter, r *http.Request) { } stackName := fmt.Sprintf("customer-%s", remoteUser) - if err := a.swarm.DeployStack(stackName, remoteUser, a.cfg.TraefikDomain); err != nil { + if err := a.swarm.DeployStack(stackName, remoteUser, a.cfg.CustomerDomain); err != nil { log.Printf("activate: stack deploy failed for %s: %v", remoteUser, err) } diff --git a/docker/ss-atlas/internal/handlers/dashboard.go b/docker/ss-atlas/internal/handlers/dashboard.go index 2c28a1a..4f53258 100644 --- a/docker/ss-atlas/internal/handlers/dashboard.go +++ b/docker/ss-atlas/internal/handlers/dashboard.go @@ -3,6 +3,8 @@ package handlers import ( "log" "net/http" + + "git.nixc.us/a250/ss-atlas/internal/version" ) func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) { @@ -18,6 +20,8 @@ func (a *App) handleDashboard(w http.ResponseWriter, r *http.Request) { "Groups": remoteGroups, "Domain": a.cfg.TraefikDomain, "IsSubscribed": remoteGroups != "" && contains(remoteGroups, "customers"), + "Commit": version.Commit, + "BuildTime": version.BuildTime, } if err := a.tmpl.ExecuteTemplate(w, "dashboard.html", data); err != nil { diff --git a/docker/ss-atlas/internal/handlers/routes.go b/docker/ss-atlas/internal/handlers/routes.go index 4358fae..c0aaa19 100644 --- a/docker/ss-atlas/internal/handlers/routes.go +++ b/docker/ss-atlas/internal/handlers/routes.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "html/template" "net/http" "path/filepath" @@ -9,6 +10,7 @@ import ( "git.nixc.us/a250/ss-atlas/internal/ldap" ssstripe "git.nixc.us/a250/ss-atlas/internal/stripe" "git.nixc.us/a250/ss-atlas/internal/swarm" + "git.nixc.us/a250/ss-atlas/internal/version" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) @@ -50,6 +52,13 @@ func NewRouter(cfg *config.Config, sc *ssstripe.Client, lc *ldap.Client, sw *swa w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) + r.Get("/version", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "commit": version.Commit, + "build_time": version.BuildTime, + }) + }) return r } diff --git a/docker/ss-atlas/internal/handlers/subscription.go b/docker/ss-atlas/internal/handlers/subscription.go index c9e0ca4..da88960 100644 --- a/docker/ss-atlas/internal/handlers/subscription.go +++ b/docker/ss-atlas/internal/handlers/subscription.go @@ -4,11 +4,15 @@ import ( "log" "net/http" "strings" + + "git.nixc.us/a250/ss-atlas/internal/version" ) func (a *App) handleLanding(w http.ResponseWriter, r *http.Request) { data := map[string]any{ - "AppURL": a.cfg.AppURL, + "AppURL": a.cfg.AppURL, + "Commit": version.Commit, + "BuildTime": version.BuildTime, } if err := a.tmpl.ExecuteTemplate(w, "landing.html", data); err != nil { log.Printf("template error: %v", err) diff --git a/docker/ss-atlas/internal/version/version.go b/docker/ss-atlas/internal/version/version.go new file mode 100644 index 0000000..3ac4085 --- /dev/null +++ b/docker/ss-atlas/internal/version/version.go @@ -0,0 +1,10 @@ +package version + +var ( + Commit = "unknown" + BuildTime = "unknown" +) + +func String() string { + return Commit + " (built " + BuildTime + ")" +} diff --git a/docker/ss-atlas/templates/pages/dashboard.html b/docker/ss-atlas/templates/pages/dashboard.html index 57ee6cf..b5b3476 100644 --- a/docker/ss-atlas/templates/pages/dashboard.html +++ b/docker/ss-atlas/templates/pages/dashboard.html @@ -101,6 +101,17 @@ .actions { display: flex; gap: 0.75rem; margin-top: 1rem; flex-wrap: wrap; } .empty-state { text-align: center; padding: 3rem 1rem; color: var(--muted); } .empty-state p { margin-bottom: 1.5rem; } + .version-badge { + position: fixed; + bottom: 0.75rem; + right: 0.75rem; + font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 0.65rem; + color: var(--muted); + opacity: 0.5; + pointer-events: none; + user-select: all; + } @@ -157,5 +168,6 @@ {{end}} +
{{.Commit}}
diff --git a/docker/ss-atlas/templates/pages/landing.html b/docker/ss-atlas/templates/pages/landing.html index 1c4af5a..ba351c9 100644 --- a/docker/ss-atlas/templates/pages/landing.html +++ b/docker/ss-atlas/templates/pages/landing.html @@ -78,6 +78,17 @@ button:hover { background: var(--accent-hover); } .footer { margin-top: 2rem; color: var(--muted); font-size: 0.8rem; text-align: center; } .footer a { color: var(--accent); text-decoration: none; } + .version-badge { + position: fixed; + bottom: 0.75rem; + right: 0.75rem; + font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; + font-size: 0.65rem; + color: var(--muted); + opacity: 0.5; + pointer-events: none; + user-select: all; + } @@ -102,5 +113,6 @@ Already subscribed? Go to Dashboard +
{{.Commit}}
diff --git a/scripts/deploy-stack-dev.sh b/scripts/deploy-stack-dev.sh new file mode 100755 index 0000000..1d98485 --- /dev/null +++ b/scripts/deploy-stack-dev.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Deploy ss-atlas + infra in local swarm mode for testing subscribe → deploy → teardown. +# Customer stacks get clientname.app.a250.ca. Run scripts/local-dns-setup.sh first for DNS. +set -e + +cd "$(dirname "$0")/.." + +if [ -n "$(git status --porcelain)" ]; then + echo "ERROR: Working tree is dirty. Commit your changes before deploying." >&2 + exit 1 +fi + +BUILD_COMMIT="$(git rev-parse --short HEAD)" +BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "=== Building commit $BUILD_COMMIT ===" + +echo "=== Ensuring swarm mode ===" +docker info --format '{{.Swarm.LocalNodeState}}' | grep -q "active" || docker swarm init + +echo "=== Creating overlay network for customer stacks ===" +docker network inspect authelia_dev >/dev/null 2>&1 || \ + docker network create -d overlay --attachable authelia_dev + +echo "=== Building ss-atlas ===" +docker compose -f docker-compose.dev.yml build \ + --build-arg BUILD_COMMIT="$BUILD_COMMIT" \ + --build-arg BUILD_TIME="$BUILD_TIME" \ + ss-atlas + +echo "=== Deploying with swarm overlay ===" +docker compose -f docker-compose.dev.yml -f docker-compose.swarm-dev.yml up -d + +echo "" +echo "=== Ready. Test flow: ===" +echo " 1. Add /etc/hosts or dnsmasq: *.app.a250.ca, app.bc.a250.ca, login.bc.a250.ca -> 127.0.0.1" +echo " 2. Visit http://app.bc.a250.ca, subscribe (Stripe test), activate" +echo " 3. After activate, customer stack deploys -> http://.app.a250.ca" +echo " 4. Cancel subscription -> webhook tears down stack" diff --git a/scripts/local-dns-setup.sh b/scripts/local-dns-setup.sh new file mode 100755 index 0000000..f4837df --- /dev/null +++ b/scripts/local-dns-setup.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# Local DNS setup so *.app.a250.ca and *.bc.a250.ca resolve to 127.0.0.1 +# For full wildcard support use dnsmasq. For quick test, use /etc/hosts (limited). +set -e + +echo "Option 1: dnsmasq (recommended - true wildcard)" +echo " Add to /etc/dnsmasq.conf or /usr/local/etc/dnsmasq.conf:" +echo " address=/.app.a250.ca/127.0.0.1" +echo " address=/.bc.a250.ca/127.0.0.1" +echo " Then: brew services restart dnsmasq" +echo " And point your Mac to use 127.0.0.1 for DNS, or add to /etc/resolvers/app.a250.ca" +echo "" +echo "Option 2: /etc/hosts (manual per-subdomain)" +echo " Add lines for each host you need:" +echo " 127.0.0.1 app.bc.a250.ca login.bc.a250.ca" +echo " 127.0.0.1 testuser.app.a250.ca" +echo " /etc/hosts does NOT support wildcards." +echo "" +echo "Option 3: resolvers file for *.app.a250.ca only" +echo " mkdir -p /etc/resolver" +echo " echo 'nameserver 127.0.0.1' | sudo tee /etc/resolver/app.a250.ca" +echo " (requires dnsmasq listening on 127.0.0.1 with address=/.app.a250.ca/127.0.0.1)" diff --git a/scripts/local-rebuild-deploy.sh b/scripts/local-rebuild-deploy.sh new file mode 100755 index 0000000..cbafadc --- /dev/null +++ b/scripts/local-rebuild-deploy.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# Static workflow: clean (stack rm + prune) → rebuild (on deploy context) → redeploy. No push; uses local images. +# Set DOCKER_DEPLOY_CONTEXT for stack target (default: default). +# Requires: swarm mode, secrets/networks on deploy node. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +STACK_NAME="${STACK_NAME:-authelia}" +DEPLOY_CTX="${DOCKER_DEPLOY_CONTEXT:-orbstack}" +STACK_RM_WAIT=15 + +cd "$REPO_ROOT" + +if [ -n "$(git status --porcelain)" ]; then + echo "ERROR: Working tree is dirty. Commit your changes before deploying." >&2 + exit 1 +fi + +BUILD_COMMIT="$(git rev-parse --short HEAD)" +BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "=== Building commit $BUILD_COMMIT ===" + +clean_on_deploy_context() { + docker context use "$DEPLOY_CTX" + docker stack rm "$STACK_NAME" 2>/dev/null || true + sleep "$STACK_RM_WAIT" + docker image prune -a -f 2>/dev/null || true + for vol in authelia_authelia_config authelia_authelia_assets authelia_authelia_redis_data authelia_authelia_mariadb_data authelia_lldap_data; do + docker volume rm "$vol" 2>/dev/null || true + done + for name in AUTHENTICATION_BACKEND_LDAP_PASSWORD IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET NOTIFIER_SMTP_PASSWORD SESSION_SECRET STORAGE_ENCRYPTION_KEY; do + docker secret rm "$name" 2>/dev/null || true + done +} + +build_on_deploy_context() { + docker context use "$DEPLOY_CTX" + docker compose -f docker-compose.production.yml build --no-cache \ + --build-arg BUILD_COMMIT="$BUILD_COMMIT" \ + --build-arg BUILD_TIME="$BUILD_TIME" +} + +ensure_external_networks() { + for net in traefik ad; do + docker network inspect "$net" --format '{{.Name}}' 2>/dev/null | grep -q . || docker network create "$net" --driver overlay --attachable + done +} + +ensure_secrets() { + LDAP_PW="/ETAToLiZPWo6QK171abAUqsa3WDpd9IgneZnTA4zU0=" + echo "$LDAP_PW" | docker secret create AUTHENTICATION_BACKEND_LDAP_PASSWORD - 2>/dev/null || true + openssl rand -base64 32 | docker secret create IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET - 2>/dev/null || true + echo "not-configured" | docker secret create NOTIFIER_SMTP_PASSWORD - 2>/dev/null || true + openssl rand -base64 32 | docker secret create SESSION_SECRET - 2>/dev/null || true + openssl rand -base64 32 | docker secret create STORAGE_ENCRYPTION_KEY - 2>/dev/null || true +} + +deploy_on_deploy_context() { + docker context use "$DEPLOY_CTX" + docker info --format '{{.Swarm.LocalNodeState}}' | grep -q active || docker swarm init + ensure_external_networks + ensure_secrets + docker stack deploy --with-registry-auth -c ./stack.production.yml "$STACK_NAME" + docker stack ps "$STACK_NAME" +} + +echo "=== Clean (stack rm + prune) on context: $DEPLOY_CTX ===" +clean_on_deploy_context + +echo "=== Rebuild (on $DEPLOY_CTX, local images) ===" +build_on_deploy_context + +echo "=== Redeploy on context: $DEPLOY_CTX ===" +deploy_on_deploy_context + +echo "=== Done ===" diff --git a/stack.production.yml b/stack.production.yml index a596c48..4860062 100644 --- a/stack.production.yml +++ b/stack.production.yml @@ -3,7 +3,7 @@ x-authelia-env: &authelia-env X_AUTHELIA_SITE_NAME: ATLAS X_AUTHELIA_CONFIG_FILTERS: template X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca - TRAEFIK_DOMAIN: a250.ca + TRAEFIK_DOMAIN: bc.a250.ca secrets: AUTHENTICATION_BACKEND_LDAP_PASSWORD: @@ -51,6 +51,8 @@ volumes: driver: local authelia_mariadb_data: driver: local + lldap_data: + driver: local services: authelia: @@ -60,6 +62,7 @@ services: - --config=/config/configuration.server.yml - --config=/config/configuration.ldap.yml - --config=/config/configuration.acl.yml + - --config=/config/configuration.notifier.yml secrets: - AUTHENTICATION_BACKEND_LDAP_PASSWORD - IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET @@ -91,24 +94,19 @@ services: restart_policy: condition: on-failure replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca labels: us.a250.autodeploy: "true" homepage.group: Infrastructure homepage.name: Authelia - homepage.href: https://login.a250.ca + homepage.href: https://login.bc.a250.ca homepage.description: ATLAS traefik.enable: "true" traefik.docker.network: traefik - traefik.http.routers.authelia_authelia.rule: Host(`login.a250.ca`) - traefik.http.routers.authelia_authelia.entrypoints: websecure - traefik.http.routers.authelia_authelia.tls: "true" - traefik.http.routers.authelia_authelia.tls.certresolver: letsencryptresolver + traefik.http.routers.authelia_authelia.rule: Host(`login.bc.a250.ca`) + traefik.http.routers.authelia_authelia.entrypoints: web traefik.http.routers.authelia_authelia.service: authelia_authelia traefik.http.services.authelia_authelia.loadbalancer.server.port: 9091 - traefik.http.middlewares.authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.a250.ca/ + traefik.http.middlewares.authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.bc.a250.ca/ traefik.http.middlewares.authelia_authelia.forwardauth.trustForwardHeader: "true" traefik.http.middlewares.authelia_authelia.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email traefik.http.middlewares.authelia-basic.forwardauth.address: http://authelia_authelia:9091/api/verify?auth=basic @@ -141,9 +139,6 @@ services: restart_policy: condition: on-failure replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca labels: us.a250.autodeploy: "true" traefik.enable: "false" @@ -159,6 +154,26 @@ services: max-size: 10m max-file: "3" + lldap: + image: nitnelave/lldap:latest + volumes: + - lldap_data:/data + environment: + LLDAP_JWT_SECRET: I2sNvGvhzZlTJWPfNL9MBPFGhyG/gWU5wHz6wFsIC3I= + LLDAP_LDAP_USER_PASS: /ETAToLiZPWo6QK171abAUqsa3WDpd9IgneZnTA4zU0= + LLDAP_LDAP_BASE_DN: dc=a250,dc=ca + networks: + - default + deploy: + restart_policy: + condition: on-failure + replicas: 1 + logging: + driver: json-file + options: + max-size: 10m + max-file: "3" + mariadb: image: git.nixc.us/a250/authelia:production-mariadb environment: @@ -178,9 +193,6 @@ services: restart_policy: condition: on-failure replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca labels: us.a250.autodeploy: "true" traefik.enable: "false" diff --git a/stack.staging.yml b/stack.staging.yml deleted file mode 100644 index c39e3fa..0000000 --- a/stack.staging.yml +++ /dev/null @@ -1,130 +0,0 @@ -x-authelia-env: &authelia-env - X_AUTHELIA_EMAIL: authelia@a250.ca - X_AUTHELIA_SITE_NAME: ATLAS - X_AUTHELIA_CONFIG_FILTERS: template - X_AUTHELIA_LDAP_DOMAIN: dc=a250,dc=ca - TRAEFIK_DOMAIN: a250.ca - -networks: - default: - driver: overlay - traefik: - external: true - ad: - external: true - -volumes: - authelia_staging_config: - driver: local - authelia_staging_redis_data: - driver: local - authelia_staging_mariadb_data: - driver: local - -services: - authelia: - image: git.nixc.us/a250/authelia:staging-authelia - command: - - authelia - - --config=/config/configuration.server.yml - - --config=/config/configuration.ldap.yml - - --config=/config/configuration.acl.yml - - --config=/config/configuration.identity.providers.yml - - --config=/config/configuration.oidc.clients.yml - environment: *authelia-env - volumes: - - authelia_staging_config:/config:rw - networks: - - traefik - - default - - ad - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca - labels: - us.a250.autodeploy: "true" - traefik.enable: "true" - traefik.docker.network: traefik - traefik.http.routers.staging-authelia_authelia.rule: Host(`staging.login.a250.ca`) - traefik.http.routers.staging-authelia_authelia.entrypoints: websecure - traefik.http.routers.staging-authelia_authelia.tls: "true" - traefik.http.routers.staging-authelia_authelia.tls.certresolver: letsencryptresolver - traefik.http.routers.staging-authelia_authelia.service: authelia_authelia - traefik.http.services.staging-authelia_authelia.loadbalancer.server.port: 9091 - traefik.http.middlewares.staging-authelia_authelia.forwardauth.address: http://authelia_authelia:9091/api/verify?rd=https://login.a250.ca/ - traefik.http.middlewares.staging-authelia_authelia.forwardauth.trustForwardHeader: "true" - traefik.http.middlewares.staging-authelia_authelia.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email - traefik.http.middlewares.staging-authelia-basic.forwardauth.address: http://authelia_authelia:9091/api/verify?auth=basic - traefik.http.middlewares.staging-authelia-basic.forwardauth.trustForwardHeader: "true" - traefik.http.middlewares.staging-authelia-basic.forwardauth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - redis: - image: git.nixc.us/a250/authelia:staging-redis - command: redis-server --appendonly yes - volumes: - - authelia_staging_redis_data:/data:rw - networks: - - default - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca - labels: - us.a250.autodeploy: "true" - traefik.enable: "false" - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" - - mariadb: - image: git.nixc.us/a250/authelia:staging-mariadb - environment: - MYSQL_ROOT_PASSWORD: authelia - MYSQL_DATABASE: authelia - MYSQL_USER: authelia - MYSQL_PASSWORD: authelia - volumes: - - authelia_staging_mariadb_data:/var/lib/mysql:rw - networks: - - default - deploy: - update_config: - order: start-first - failure_action: rollback - parallelism: 1 - restart_policy: - condition: on-failure - replicas: 1 - placement: - constraints: - - node.hostname == ingress.a250.ca - labels: - us.a250.autodeploy: "true" - traefik.enable: "false" - logging: - driver: json-file - options: - max-size: 10m - max-file: "3" \ No newline at end of file