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