@yunuseon/kayra 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/ansible/.ansible-lint +13 -0
- package/ansible/ansible.cfg +12 -0
- package/ansible/playbooks/adopt.yml +35 -0
- package/ansible/playbooks/app.yml +21 -0
- package/ansible/playbooks/apps-prune.yml +30 -0
- package/ansible/playbooks/edge.yml +41 -0
- package/ansible/playbooks/nas.yml +47 -0
- package/ansible/playbooks/platform.yml +62 -0
- package/ansible/requirements.yml +7 -0
- package/ansible/roles/app/defaults/main.yml +9 -0
- package/ansible/roles/app/tasks/main.yml +107 -0
- package/ansible/roles/app/templates/app.env.j2 +9 -0
- package/ansible/roles/app/templates/casc-app.yaml.j2 +68 -0
- package/ansible/roles/backup-check/files/backup-check.service +9 -0
- package/ansible/roles/backup-check/files/backup-check.timer +10 -0
- package/ansible/roles/backup-check/tasks/main.yml +59 -0
- package/ansible/roles/backup-check/templates/backup-check.sh.j2 +117 -0
- package/ansible/roles/common/handlers/main.yml +16 -0
- package/ansible/roles/common/tasks/main.yml +100 -0
- package/ansible/roles/compose-stacks/tasks/main.yml +74 -0
- package/ansible/roles/docker/defaults/main.yml +3 -0
- package/ansible/roles/docker/files/buildkit-rootless +23 -0
- package/ansible/roles/docker/handlers/main.yml +12 -0
- package/ansible/roles/docker/tasks/main.yml +82 -0
- package/ansible/roles/edge-proxy/files/docker-compose.yml +13 -0
- package/ansible/roles/edge-proxy/handlers/main.yml +6 -0
- package/ansible/roles/edge-proxy/tasks/main.yml +34 -0
- package/ansible/roles/edge-proxy/templates/Caddyfile.j2 +25 -0
- package/ansible/roles/forgejo-config/tasks/main.yml +292 -0
- package/ansible/roles/platform/defaults/main.yml +76 -0
- package/ansible/roles/platform/tasks/main.yml +298 -0
- package/ansible/roles/platform/templates/authelia-users.yml.j2 +17 -0
- package/ansible/roles/platform/templates/platform.env.j2 +41 -0
- package/ansible/roles/restic-backup/files/docker-compose.postgres.yml +27 -0
- package/ansible/roles/restic-backup/files/docker-compose.yml +12 -0
- package/ansible/roles/restic-backup/handlers/main.yml +7 -0
- package/ansible/roles/restic-backup/tasks/main.yml +121 -0
- package/ansible/roles/restic-backup/templates/backup.env.j2 +9 -0
- package/ansible/roles/restic-backup/templates/backup.sh.j2 +28 -0
- package/ansible/roles/restic-backup/templates/docker-compose.override.yml.j2 +26 -0
- package/ansible/roles/restic-backup/templates/follow-restore-test.sh.j2 +28 -0
- package/ansible/roles/restic-backup/templates/restore-test.sh.j2 +51 -0
- package/ansible/roles/restic-backup/templates/root.crontab.j2 +6 -0
- package/ansible/roles/restic-backup/templates/ssh_config.j2 +12 -0
- package/ansible/roles/wireguard/handlers/main.yml +9 -0
- package/ansible/roles/wireguard/tasks/main.yml +33 -0
- package/ansible/roles/wireguard/templates/wg0.conf.j2 +12 -0
- package/bin/kayra.js +29 -0
- package/cli/dist/brand.js +41 -0
- package/cli/dist/cli.js +21 -0
- package/cli/dist/commands/doctor.js +79 -0
- package/cli/dist/commands/server.js +215 -0
- package/cli/dist/commands/status.js +43 -0
- package/cli/dist/fleet.js +134 -0
- package/cli/dist/interpret.js +98 -0
- package/cli/dist/load.js +49 -0
- package/cli/dist/main.js +94 -0
- package/cli/dist/plan.js +31 -0
- package/cli/dist/result.js +5 -0
- package/cloud-init.yml.tpl +14 -0
- package/compose/authelia/configuration.yml +138 -0
- package/compose/buildkit/buildkitd.toml +14 -0
- package/compose/docker-compose.acme-dns.yml +12 -0
- package/compose/docker-compose.ci.yml +156 -0
- package/compose/docker-compose.yml +83 -0
- package/compose/jenkins/Dockerfile +40 -0
- package/compose/jenkins/casc/00-platform.yaml +158 -0
- package/compose/jenkins/plugins.txt +97 -0
- package/compose/jenkins-agent/Dockerfile +24 -0
- package/compose/traefik/dynamic/authelia.yml +18 -0
- package/compose/traefik/dynamic/middlewares.yml +14 -0
- package/compose/traefik/dynamic/ratelimit.yml +21 -0
- package/compose/traefik/dynamic/tls.yml +9 -0
- package/compose/traefik/traefik.yml +46 -0
- package/hosts.conf +6 -0
- package/package.json +73 -4
- package/scripts/local/app-manifests.sh +26 -0
- package/scripts/local/authelia-dev-init.sh +58 -0
- package/scripts/local/certs.sh +55 -0
- package/scripts/local/home-image.sh +55 -0
- package/scripts/local/home-reach.sh +41 -0
- package/scripts/local/require-main.sh +38 -0
- package/scripts/local/setup-hosts.sh +30 -0
- package/scripts/server/app-purge +26 -0
- package/scripts/server/app-remove +30 -0
- package/scripts/server/jenkins-api +28 -0
- package/scripts/server/jenkins-app-apply +36 -0
- package/scripts/server/jenkins-reload +12 -0
- package/terraform/modules/hcloud-firewall/main.tf +54 -0
- package/terraform/modules/hcloud-firewall/outputs.tf +9 -0
- package/terraform/modules/hcloud-firewall/variables.tf +22 -0
- package/terraform/modules/hcloud-server/main.tf +62 -0
- package/terraform/modules/hcloud-server/outputs.tf +19 -0
- package/terraform/modules/hcloud-server/templates/inventory.ini.tpl +5 -0
- package/terraform/modules/hcloud-server/variables.tf +50 -0
- package/terraform/modules/multipass-instance/main.tf +67 -0
- package/terraform/modules/multipass-instance/outputs.tf +19 -0
- package/terraform/modules/multipass-instance/templates/inventory.ini.tpl +5 -0
- package/terraform/modules/multipass-instance/variables.tf +39 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#cloud-config
|
|
2
|
+
# Only what Ansible needs to get in. Everything else (packages, Docker, swap,
|
|
3
|
+
# SSH hardening) is the common and docker roles' job: they run on every
|
|
4
|
+
# deploy, while this runs once on first boot and can never be re-applied.
|
|
5
|
+
|
|
6
|
+
hostname: ${hostname}
|
|
7
|
+
|
|
8
|
+
users:
|
|
9
|
+
- name: deploy
|
|
10
|
+
groups: sudo
|
|
11
|
+
shell: /bin/bash
|
|
12
|
+
sudo: ALL=(ALL) NOPASSWD:ALL
|
|
13
|
+
ssh_authorized_keys:
|
|
14
|
+
- ${ssh_pubkey}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Authelia — login for the platform's own pages (Jenkins, Traefik's
|
|
3
|
+
# dashboard). Apps bring their own login; their users never live here.
|
|
4
|
+
#
|
|
5
|
+
# Authelia does NOT expand ${VAR} the way docker compose does. Interpolation here
|
|
6
|
+
# comes from the `template` config filter (X_AUTHELIA_CONFIG_FILTERS=template),
|
|
7
|
+
# which is Go templating: {{ env "DOMAIN" }}.
|
|
8
|
+
#
|
|
9
|
+
# Secrets are never in this file — they arrive as AUTHELIA_*_FILE env vars.
|
|
10
|
+
# =============================================================================
|
|
11
|
+
|
|
12
|
+
theme: 'auto'
|
|
13
|
+
|
|
14
|
+
server:
|
|
15
|
+
address: 'tcp://0.0.0.0:9091/'
|
|
16
|
+
|
|
17
|
+
log:
|
|
18
|
+
level: 'info'
|
|
19
|
+
format: 'text'
|
|
20
|
+
|
|
21
|
+
# Passkeys are the point of this setup; TOTP and Duo would only be extra
|
|
22
|
+
# surface to configure and explain.
|
|
23
|
+
totp:
|
|
24
|
+
disable: true
|
|
25
|
+
duo_api:
|
|
26
|
+
disable: true
|
|
27
|
+
|
|
28
|
+
webauthn:
|
|
29
|
+
disable: false
|
|
30
|
+
enable_passkey_login: true
|
|
31
|
+
# A passkey assertion carrying the user-verification flag satisfies the
|
|
32
|
+
# two_factor policy on its own, so login is one Touch ID prompt rather than
|
|
33
|
+
# password-then-passkey. A stolen bootstrap password alone reaches nothing.
|
|
34
|
+
experimental_enable_passkey_uv_two_factors: true
|
|
35
|
+
display_name: '{{ env "DOMAIN" }}'
|
|
36
|
+
attestation_conveyance_preference: 'indirect'
|
|
37
|
+
timeout: '60 seconds'
|
|
38
|
+
selection_criteria:
|
|
39
|
+
attachment: ''
|
|
40
|
+
discoverability: 'required'
|
|
41
|
+
# 'required', not the default 'preferred': with 'preferred' an authenticator
|
|
42
|
+
# may omit the user-verification flag, and the user is then stuck unable to
|
|
43
|
+
# satisfy a two_factor policy.
|
|
44
|
+
user_verification: 'required'
|
|
45
|
+
|
|
46
|
+
identity_validation:
|
|
47
|
+
elevated_session:
|
|
48
|
+
code_lifespan: '5 minutes'
|
|
49
|
+
elevation_lifespan: '10 minutes'
|
|
50
|
+
characters: 8
|
|
51
|
+
require_second_factor: false
|
|
52
|
+
# Once someone has one passkey, registering a second skips the one-time
|
|
53
|
+
# code entirely.
|
|
54
|
+
skip_second_factor: true
|
|
55
|
+
|
|
56
|
+
authentication_backend:
|
|
57
|
+
refresh_interval: 'always'
|
|
58
|
+
# Both disabled so Authelia never writes back to users_database.yml. That
|
|
59
|
+
# file is rendered from vault on every platform run and would otherwise be
|
|
60
|
+
# silently reverted.
|
|
61
|
+
password_reset:
|
|
62
|
+
disable: true
|
|
63
|
+
password_change:
|
|
64
|
+
disable: true
|
|
65
|
+
file:
|
|
66
|
+
path: '/config/users_database.yml'
|
|
67
|
+
watch: true
|
|
68
|
+
search:
|
|
69
|
+
email: true
|
|
70
|
+
case_insensitive: true
|
|
71
|
+
password:
|
|
72
|
+
algorithm: 'argon2'
|
|
73
|
+
argon2:
|
|
74
|
+
variant: 'argon2id'
|
|
75
|
+
iterations: 3
|
|
76
|
+
# KiB, allocated per concurrent hash. The default 64 MiB is too fat for
|
|
77
|
+
# a 4 GB box with no headroom; 32 MiB is still strong for a credential
|
|
78
|
+
# used once per person, at enrolment.
|
|
79
|
+
memory: 32768
|
|
80
|
+
parallelism: 2
|
|
81
|
+
key_length: 32
|
|
82
|
+
salt_length: 16
|
|
83
|
+
|
|
84
|
+
# AUTH_POLICY is always set by the platform role (default two_factor); test
|
|
85
|
+
# lowers it to one_factor so staging does not need passkey enrolment.
|
|
86
|
+
access_control:
|
|
87
|
+
default_policy: 'deny'
|
|
88
|
+
rules:
|
|
89
|
+
- domain: 'jenkins.{{ env "DOMAIN" }}'
|
|
90
|
+
policy: '{{ env "AUTH_POLICY" }}'
|
|
91
|
+
subject:
|
|
92
|
+
- 'group:admin'
|
|
93
|
+
- domain: 'traefik.{{ env "DOMAIN" }}'
|
|
94
|
+
policy: '{{ env "AUTH_POLICY" }}'
|
|
95
|
+
subject:
|
|
96
|
+
- 'group:admin'
|
|
97
|
+
|
|
98
|
+
session:
|
|
99
|
+
name: 'authelia_session'
|
|
100
|
+
same_site: 'lax'
|
|
101
|
+
# A working day of idle time. Shorter is tempting, but a session that dies
|
|
102
|
+
# mid-form turns the next POST into a 302 to the portal and the body is lost.
|
|
103
|
+
inactivity: '8 hours'
|
|
104
|
+
expiration: '1 day'
|
|
105
|
+
remember_me: '1 week'
|
|
106
|
+
cookies:
|
|
107
|
+
# The apex, not auth.<DOMAIN> — the cookie has to reach jenkins.<DOMAIN>
|
|
108
|
+
# and traefik.<DOMAIN> too. Authelia requires this to be a suffix of (or equal to) authelia_url.
|
|
109
|
+
- domain: '{{ env "DOMAIN" }}'
|
|
110
|
+
authelia_url: 'https://auth.{{ env "DOMAIN" }}'
|
|
111
|
+
|
|
112
|
+
regulation:
|
|
113
|
+
max_retries: 3
|
|
114
|
+
find_time: '2 minutes'
|
|
115
|
+
ban_time: '5 minutes'
|
|
116
|
+
|
|
117
|
+
storage:
|
|
118
|
+
local:
|
|
119
|
+
path: '/data/db.sqlite3'
|
|
120
|
+
|
|
121
|
+
# SMTP when it is configured, otherwise write to a file. The notifier is only
|
|
122
|
+
# used for the one-time code at passkey enrolment, so the file fallback keeps
|
|
123
|
+
# dev working without an external dependency.
|
|
124
|
+
notifier:
|
|
125
|
+
disable_startup_check: false
|
|
126
|
+
{{- if env "SMTP_ADDRESS" }}
|
|
127
|
+
smtp:
|
|
128
|
+
address: '{{ env "SMTP_ADDRESS" }}'
|
|
129
|
+
username: '{{ env "SMTP_USERNAME" }}'
|
|
130
|
+
sender: '{{ env "SMTP_SENDER" }}'
|
|
131
|
+
# In the template rather than an env var on purpose: an empty
|
|
132
|
+
# AUTHELIA_NOTIFIER_SMTP_PASSWORD still creates notifier.smtp, and Authelia
|
|
133
|
+
# then refuses to start with both notifiers configured.
|
|
134
|
+
password: {{ secret "/config/secrets/smtp_password" | msquote }}
|
|
135
|
+
{{- else }}
|
|
136
|
+
filesystem:
|
|
137
|
+
filename: '/data/notification.txt'
|
|
138
|
+
{{- end }}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# BuildKit daemon config.
|
|
2
|
+
#
|
|
3
|
+
# The registry is plain HTTP on the internal docker network and has no TLS
|
|
4
|
+
# anywhere. BuildKit has to be told that explicitly or it tries HTTPS.
|
|
5
|
+
|
|
6
|
+
[worker.oci]
|
|
7
|
+
enabled = true
|
|
8
|
+
# Rootless: no process sandbox, and the rootless worker cannot use overlayfs
|
|
9
|
+
# on every kernel, so let BuildKit pick a snapshotter it can actually run.
|
|
10
|
+
max-parallelism = 2
|
|
11
|
+
|
|
12
|
+
[registry."registry:5000"]
|
|
13
|
+
http = true
|
|
14
|
+
insecure = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Overlay for a server whose certificates come by DNS-01 (acme_dns in its
|
|
3
|
+
# vars.yml): lego, inside Traefik, reads the zone's token from a file. Added to
|
|
4
|
+
# COMPOSE_FILE only there, so other servers' Traefik stays as it is.
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
services:
|
|
8
|
+
traefik:
|
|
9
|
+
environment:
|
|
10
|
+
HETZNER_API_TOKEN_FILE: /run/secrets/acme-dns-token
|
|
11
|
+
volumes:
|
|
12
|
+
- ./traefik/secrets:/run/secrets:ro
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Platform CI overlay — Docker Registry + Jenkins (controller + agent)
|
|
3
|
+
# App-agnostic: provides CI/CD infrastructure for any application.
|
|
4
|
+
# Usage: docker compose -f docker-compose.yml -f docker-compose.ci.yml up
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
services:
|
|
8
|
+
registry:
|
|
9
|
+
image: registry:2.8.3
|
|
10
|
+
restart: unless-stopped
|
|
11
|
+
environment:
|
|
12
|
+
# registry-gc.sh deletes manifests nothing references any more.
|
|
13
|
+
- REGISTRY_STORAGE_DELETE_ENABLED=true
|
|
14
|
+
# No Traefik router: builds push to registry:5000 and the host daemon pulls
|
|
15
|
+
# from localhost:5000, so nothing outside the server needs it.
|
|
16
|
+
ports:
|
|
17
|
+
- "127.0.0.1:5000:5000"
|
|
18
|
+
volumes:
|
|
19
|
+
- registry-data:/var/lib/registry
|
|
20
|
+
networks:
|
|
21
|
+
- internal
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# BuildKit — builds images without anyone holding the host's docker socket.
|
|
25
|
+
#
|
|
26
|
+
# The build agent used to mount /var/run/docker.sock read-write while running
|
|
27
|
+
# pipeline code checked out from arbitrary branches, which is host root by
|
|
28
|
+
# design rather than by exploit. Builds now go here instead: rootless, its own
|
|
29
|
+
# daemon, pushing straight to the registry. Compromising it gets you a
|
|
30
|
+
# container, not the box.
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
buildkitd:
|
|
33
|
+
image: moby/buildkit:v0.34.0-rootless
|
|
34
|
+
restart: unless-stopped
|
|
35
|
+
# Flags go in the command: this image does not read BUILDKITD_FLAGS (that
|
|
36
|
+
# is a buildx convention), and without them it listens on a unix socket
|
|
37
|
+
# the agent cannot reach.
|
|
38
|
+
command:
|
|
39
|
+
- --oci-worker-no-process-sandbox
|
|
40
|
+
- --addr
|
|
41
|
+
- tcp://0.0.0.0:1234
|
|
42
|
+
# rootlesskit needs an unprivileged user namespace, which Ubuntu 23.10+
|
|
43
|
+
# denies to unconfined processes. The named profile (installed by the docker
|
|
44
|
+
# role) grants userns to this container alone, so the host keeps the
|
|
45
|
+
# restriction.
|
|
46
|
+
security_opt:
|
|
47
|
+
- seccomp=unconfined
|
|
48
|
+
- apparmor=buildkit-rootless
|
|
49
|
+
volumes:
|
|
50
|
+
- buildkit-cache:/home/user/.local/share/buildkit
|
|
51
|
+
- ./buildkit/buildkitd.toml:/home/user/.config/buildkit/buildkitd.toml:ro
|
|
52
|
+
mem_limit: 1g
|
|
53
|
+
networks:
|
|
54
|
+
- internal
|
|
55
|
+
|
|
56
|
+
jenkins-controller:
|
|
57
|
+
build:
|
|
58
|
+
context: ./jenkins
|
|
59
|
+
dockerfile: Dockerfile
|
|
60
|
+
# The controller runs as jenkins (uid/gid 1000) and reaches the socket via
|
|
61
|
+
# the host's docker group, whose GID differs per host.
|
|
62
|
+
group_add:
|
|
63
|
+
- ${DOCKER_GID:?}
|
|
64
|
+
restart: unless-stopped
|
|
65
|
+
# No COMPOSE_PROJECT_NAME here on purpose: every app's deploy runs
|
|
66
|
+
# `docker compose` from this container, and an exported project name would
|
|
67
|
+
# beat the one the deploy passes. APPS_DIR is where app .env files live.
|
|
68
|
+
environment:
|
|
69
|
+
- JENKINS_ADMIN_USER=${JENKINS_ADMIN_USER:-admin}
|
|
70
|
+
- JENKINS_ADMIN_PASSWORD=${JENKINS_ADMIN_PASSWORD:-admin}
|
|
71
|
+
- JENKINS_GIT_REPO=${GIT_REMOTE_URL:?}
|
|
72
|
+
- GITHUB_WEBHOOK_SECRET=${GITHUB_WEBHOOK_SECRET:-}
|
|
73
|
+
- DOMAIN=${DOMAIN:-localhost}
|
|
74
|
+
- APPS_DIR=${APPS_DIR:?}
|
|
75
|
+
- PLATFORM_REF=${PLATFORM_REF:?}
|
|
76
|
+
- PLATFORM_RENOVATE=${PLATFORM_RENOVATE:-false}
|
|
77
|
+
- RENOVATE_SCHEDULE=${RENOVATE_SCHEDULE:-H 4 * * 1}
|
|
78
|
+
- RENOVATE_PLATFORM=${RENOVATE_PLATFORM:-github}
|
|
79
|
+
- RENOVATE_ENDPOINT=${RENOVATE_ENDPOINT:-}
|
|
80
|
+
- RENOVATE_GIT_URL=${RENOVATE_GIT_URL:-}
|
|
81
|
+
- RENOVATE_GITHUB_COM_TOKEN=${RENOVATE_GITHUB_COM_TOKEN:-}
|
|
82
|
+
- RENOVATE_HOST_RULES=${RENOVATE_HOST_RULES:-}
|
|
83
|
+
- RENOVATE_REPOSITORIES=${RENOVATE_REPOSITORIES:-}
|
|
84
|
+
# Tell JCasC to merge every YAML in this directory at startup.
|
|
85
|
+
- CASC_JENKINS_CONFIG=/var/jenkins_home/casc
|
|
86
|
+
volumes:
|
|
87
|
+
- jenkins-home:/var/jenkins_home
|
|
88
|
+
# The controller is the only node with the docker socket, and it runs
|
|
89
|
+
# the deploy stage, so it needs the app's compose directory too.
|
|
90
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
91
|
+
- ${APPS_DIR:?}:${APPS_DIR:?}:ro
|
|
92
|
+
- ./jenkins/casc:/var/jenkins_home/casc:ro
|
|
93
|
+
# One directory per app, one file per secret; JCasC reads them with
|
|
94
|
+
# ${readFile:...}. Written by bin/jenkins-app-apply.
|
|
95
|
+
- ./jenkins-secrets:/run/app-secrets:ro
|
|
96
|
+
# GIT_TOKEN and RENOVATE_TOKEN, likewise: files, so a new one needs a
|
|
97
|
+
# reload, not a restart. Written by the platform role (or forgejo-config).
|
|
98
|
+
- ./platform-secrets:/run/platform-secrets:ro
|
|
99
|
+
labels:
|
|
100
|
+
- "traefik.enable=true"
|
|
101
|
+
- "traefik.docker.network=platform-internal"
|
|
102
|
+
- "traefik.http.routers.jenkins.rule=Host(`jenkins.${DOMAIN:-localhost}`)"
|
|
103
|
+
- "traefik.http.routers.jenkins.entrypoints=websecure"
|
|
104
|
+
- "traefik.http.routers.jenkins.tls.certresolver=le"
|
|
105
|
+
- "traefik.http.routers.jenkins.middlewares=authelia@file"
|
|
106
|
+
- "traefik.http.routers.jenkins.service=jenkins"
|
|
107
|
+
# GitHub cannot log in; the plugin checks the payload's HMAC instead.
|
|
108
|
+
- "traefik.http.routers.jenkins-webhook.rule=Host(`jenkins.${DOMAIN:-localhost}`) && Path(`/github-webhook/`)"
|
|
109
|
+
- "traefik.http.routers.jenkins-webhook.entrypoints=websecure"
|
|
110
|
+
- "traefik.http.routers.jenkins-webhook.tls.certresolver=le"
|
|
111
|
+
- "traefik.http.routers.jenkins-webhook.priority=100"
|
|
112
|
+
- "traefik.http.routers.jenkins-webhook.service=jenkins"
|
|
113
|
+
- "traefik.http.services.jenkins.loadbalancer.server.port=8080"
|
|
114
|
+
networks:
|
|
115
|
+
- internal
|
|
116
|
+
healthcheck:
|
|
117
|
+
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/login"]
|
|
118
|
+
interval: 15s
|
|
119
|
+
timeout: 5s
|
|
120
|
+
retries: 10
|
|
121
|
+
start_period: 60s
|
|
122
|
+
|
|
123
|
+
jenkins-agent:
|
|
124
|
+
build:
|
|
125
|
+
context: ./jenkins-agent
|
|
126
|
+
dockerfile: Dockerfile
|
|
127
|
+
restart: unless-stopped
|
|
128
|
+
# Written by the platform role once Jenkins is up; absent on first start,
|
|
129
|
+
# when the agent crash-loops until the role recreates it with the secret.
|
|
130
|
+
env_file:
|
|
131
|
+
- path: ./jenkins-agent.env
|
|
132
|
+
required: false
|
|
133
|
+
environment:
|
|
134
|
+
- JENKINS_URL=http://jenkins-controller:8080
|
|
135
|
+
- JENKINS_AGENT_NAME=builder
|
|
136
|
+
- JENKINS_AGENT_WORKDIR=/home/jenkins/agent
|
|
137
|
+
- DOMAIN=${DOMAIN:-localhost}
|
|
138
|
+
# Builds go to buildkitd, not to a docker daemon. This agent runs pipeline
|
|
139
|
+
# code from untrusted branches and deliberately has no docker socket.
|
|
140
|
+
- BUILDKIT_HOST=tcp://buildkitd:1234
|
|
141
|
+
- REGISTRY_PUSH_HOST=registry:5000
|
|
142
|
+
volumes:
|
|
143
|
+
- jenkins-agent-workdir:/home/jenkins/agent
|
|
144
|
+
networks:
|
|
145
|
+
- internal
|
|
146
|
+
depends_on:
|
|
147
|
+
jenkins-controller:
|
|
148
|
+
condition: service_healthy
|
|
149
|
+
buildkitd:
|
|
150
|
+
condition: service_started
|
|
151
|
+
|
|
152
|
+
volumes:
|
|
153
|
+
registry-data:
|
|
154
|
+
buildkit-cache:
|
|
155
|
+
jenkins-home:
|
|
156
|
+
jenkins-agent-workdir:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Platform base — Traefik reverse proxy + shared network
|
|
3
|
+
# App-agnostic: knows nothing about what applications run on this platform.
|
|
4
|
+
# =============================================================================
|
|
5
|
+
|
|
6
|
+
services:
|
|
7
|
+
traefik:
|
|
8
|
+
image: traefik:v3.7.14
|
|
9
|
+
restart: unless-stopped
|
|
10
|
+
ports:
|
|
11
|
+
- "80:80"
|
|
12
|
+
- "443:443"
|
|
13
|
+
volumes:
|
|
14
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
15
|
+
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
16
|
+
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
|
17
|
+
- ./traefik/certs:/certs:ro
|
|
18
|
+
- acme-data:/acme
|
|
19
|
+
networks:
|
|
20
|
+
- public
|
|
21
|
+
- internal
|
|
22
|
+
labels:
|
|
23
|
+
# The dashboard used to ride on an unauthenticated published :8080.
|
|
24
|
+
# It speaks no auth of its own, which is exactly what forwardAuth is for.
|
|
25
|
+
- "traefik.enable=true"
|
|
26
|
+
- "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN:-localhost}`)"
|
|
27
|
+
- "traefik.http.routers.dashboard.entrypoints=websecure"
|
|
28
|
+
- "traefik.http.routers.dashboard.tls.certresolver=le"
|
|
29
|
+
- "traefik.http.routers.dashboard.service=api@internal"
|
|
30
|
+
- "traefik.http.routers.dashboard.middlewares=authelia@file,security-headers@file"
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Authelia — single sign-on for services that sit behind Traefik.
|
|
34
|
+
#
|
|
35
|
+
# Deliberately NOT behind its own authelia@file middleware: that would be an
|
|
36
|
+
# infinite redirect. The portal has to stay reachable to authenticate anyone.
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
authelia:
|
|
39
|
+
image: authelia/authelia:4.39.28
|
|
40
|
+
restart: unless-stopped
|
|
41
|
+
environment:
|
|
42
|
+
- X_AUTHELIA_CONFIG=/config/configuration.yml
|
|
43
|
+
# Authelia does not expand ${VAR}; this filter is what makes
|
|
44
|
+
# {{ env "DOMAIN" }} work inside configuration.yml.
|
|
45
|
+
- X_AUTHELIA_CONFIG_FILTERS=template
|
|
46
|
+
- DOMAIN=${DOMAIN:-localhost}
|
|
47
|
+
- AUTH_POLICY=${AUTH_POLICY:-two_factor}
|
|
48
|
+
- AUTHELIA_SESSION_SECRET_FILE=/config/secrets/session
|
|
49
|
+
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/config/secrets/storage_encryption
|
|
50
|
+
# Empty in dev, which falls the notifier back to a file — see
|
|
51
|
+
# configuration.yml. The SMTP password is NOT an env var; it is read from
|
|
52
|
+
# /config/secrets/smtp_password by the config template.
|
|
53
|
+
- SMTP_ADDRESS=${AUTHELIA_SMTP_ADDRESS:-}
|
|
54
|
+
- SMTP_USERNAME=${AUTHELIA_SMTP_USERNAME:-}
|
|
55
|
+
- SMTP_SENDER=${AUTHELIA_SMTP_SENDER:-}
|
|
56
|
+
volumes:
|
|
57
|
+
- ./authelia:/config:ro
|
|
58
|
+
- authelia-data:/data
|
|
59
|
+
mem_limit: 128m
|
|
60
|
+
networks:
|
|
61
|
+
- internal
|
|
62
|
+
labels:
|
|
63
|
+
- "traefik.enable=true"
|
|
64
|
+
- "traefik.docker.network=platform-internal"
|
|
65
|
+
- "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN:-localhost}`)"
|
|
66
|
+
- "traefik.http.routers.authelia.entrypoints=websecure"
|
|
67
|
+
- "traefik.http.routers.authelia.tls.certresolver=le"
|
|
68
|
+
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
|
|
69
|
+
|
|
70
|
+
# Two networks, so an app can reach Traefik's routes and nothing else of the
|
|
71
|
+
# platform. Apps join platform-public; the platform's own services live on
|
|
72
|
+
# platform-internal, where only Traefik joins them.
|
|
73
|
+
networks:
|
|
74
|
+
public:
|
|
75
|
+
name: platform-public
|
|
76
|
+
internal:
|
|
77
|
+
name: platform-internal
|
|
78
|
+
|
|
79
|
+
volumes:
|
|
80
|
+
acme-data:
|
|
81
|
+
# Passkeys live here. Losing this volume means every credential is gone and
|
|
82
|
+
# everyone re-enrols — back it up.
|
|
83
|
+
authelia-data:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Jenkins Controller — LTS with Docker CLI, Compose, buildctl and JCasC
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
FROM jenkins/jenkins:2.580.1-lts-jdk21
|
|
6
|
+
|
|
7
|
+
USER root
|
|
8
|
+
|
|
9
|
+
# renovate: datasource=github-releases depName=moby/buildkit
|
|
10
|
+
ARG BUILDKIT_VERSION=v0.34.0
|
|
11
|
+
|
|
12
|
+
# Install Docker CLI + Compose plugin
|
|
13
|
+
RUN apt-get update && \
|
|
14
|
+
apt-get install -y --no-install-recommends \
|
|
15
|
+
ca-certificates curl gnupg jq && \
|
|
16
|
+
install -m 0755 -d /etc/apt/keyrings && \
|
|
17
|
+
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
|
|
18
|
+
chmod a+r /etc/apt/keyrings/docker.gpg && \
|
|
19
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
|
20
|
+
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
|
21
|
+
> /etc/apt/sources.list.d/docker.list && \
|
|
22
|
+
apt-get update && \
|
|
23
|
+
apt-get install -y --no-install-recommends \
|
|
24
|
+
docker-ce-cli docker-compose-plugin && \
|
|
25
|
+
arch="$(dpkg --print-architecture)" && \
|
|
26
|
+
curl -fsSL "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${arch}.tar.gz" \
|
|
27
|
+
| tar -xz -C /usr/local bin/buildctl && \
|
|
28
|
+
rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
USER jenkins
|
|
31
|
+
|
|
32
|
+
# Install plugins
|
|
33
|
+
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
|
|
34
|
+
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt
|
|
35
|
+
|
|
36
|
+
# JCasC fragments are bind-mounted, not baked in; CASC_JENKINS_CONFIG is set in
|
|
37
|
+
# compose. That keeps config out of the image and out of the jenkins-home volume.
|
|
38
|
+
# notifyCommit without a token, but only to trigger polling: how Forgejo tells
|
|
39
|
+
# Jenkins about a push, over platform-internal (Traefik does not route it).
|
|
40
|
+
ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false -Dhudson.plugins.git.GitStatus.NOTIFY_COMMIT_ACCESS_CONTROL=disabled-for-polling"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Jenkins Configuration as Code — platform fragment (app-agnostic).
|
|
3
|
+
#
|
|
4
|
+
# Owns:
|
|
5
|
+
# - Jenkins setup, security realm, authorization
|
|
6
|
+
# - The agent node
|
|
7
|
+
# - Git credentials (for cloning the app repo)
|
|
8
|
+
# - GitHub: push-webhook secret + API token for commit statuses
|
|
9
|
+
# - Jenkins URL
|
|
10
|
+
#
|
|
11
|
+
# Also owns the app-agnostic jobs: platform-registry-gc and rollback work off
|
|
12
|
+
# the release records every app's <app>/deploy job writes (the platform
|
|
13
|
+
# library's appDeploy) and start that job with a REF. platform-renovate exists
|
|
14
|
+
# only where the server has repos to keep up to date (PLATFORM_RENOVATE).
|
|
15
|
+
#
|
|
16
|
+
# Does NOT own any app-specific credentials (Postgres, ...) or app pipelines.
|
|
17
|
+
# Each app renders its own 10-<app>.yaml next to this file and applies it with
|
|
18
|
+
# bin/jenkins-app-apply. JCasC merges every YAML in this directory.
|
|
19
|
+
#
|
|
20
|
+
# Git remote comes from the platform .env (rendered by Ansible).
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
jenkins:
|
|
24
|
+
systemMessage: "Platform CI/CD — Managed by JCasC"
|
|
25
|
+
numExecutors: 1
|
|
26
|
+
|
|
27
|
+
securityRealm:
|
|
28
|
+
local:
|
|
29
|
+
allowsSignup: false
|
|
30
|
+
users:
|
|
31
|
+
- id: "${JENKINS_ADMIN_USER:-admin}"
|
|
32
|
+
password: "${JENKINS_ADMIN_PASSWORD:-admin}"
|
|
33
|
+
|
|
34
|
+
authorizationStrategy:
|
|
35
|
+
loggedInUsersCanDoAnything:
|
|
36
|
+
allowAnonymousRead: false
|
|
37
|
+
|
|
38
|
+
nodes:
|
|
39
|
+
- permanent:
|
|
40
|
+
name: "builder"
|
|
41
|
+
labelString: "buildkit linux"
|
|
42
|
+
numExecutors: 2
|
|
43
|
+
remoteFS: "/home/jenkins/agent"
|
|
44
|
+
launcher:
|
|
45
|
+
inbound:
|
|
46
|
+
workDirSettings:
|
|
47
|
+
disabled: false
|
|
48
|
+
internalDir: "remoting"
|
|
49
|
+
retentionStrategy: "always"
|
|
50
|
+
|
|
51
|
+
credentials:
|
|
52
|
+
system:
|
|
53
|
+
domainCredentials:
|
|
54
|
+
- credentials:
|
|
55
|
+
- usernamePassword:
|
|
56
|
+
scope: GLOBAL
|
|
57
|
+
id: "git-credentials"
|
|
58
|
+
description: "Git credentials for cloning repositories"
|
|
59
|
+
username: "git"
|
|
60
|
+
password: "${readFile:/run/platform-secrets/GIT_TOKEN}"
|
|
61
|
+
- string:
|
|
62
|
+
scope: GLOBAL
|
|
63
|
+
id: "github-token"
|
|
64
|
+
description: "Same PAT as git-credentials; needs Commit statuses: write"
|
|
65
|
+
secret: "${readFile:/run/platform-secrets/GIT_TOKEN}"
|
|
66
|
+
- string:
|
|
67
|
+
scope: GLOBAL
|
|
68
|
+
id: "renovate-token"
|
|
69
|
+
description: "PAT for Renovate: Contents + Pull requests read & write"
|
|
70
|
+
secret: "${readFile:/run/platform-secrets/RENOVATE_TOKEN}"
|
|
71
|
+
- string:
|
|
72
|
+
scope: GLOBAL
|
|
73
|
+
id: "github-webhook-secret"
|
|
74
|
+
description: "HMAC secret GitHub signs push webhooks with"
|
|
75
|
+
secret: "${GITHUB_WEBHOOK_SECRET:-}"
|
|
76
|
+
|
|
77
|
+
unclassified:
|
|
78
|
+
# The pipeline every app runs. Trusted (outside the sandbox), loaded from
|
|
79
|
+
# PLATFORM_REF (the release tag, v<version>; a test server may set a branch) and jobs cannot
|
|
80
|
+
# pick another version, so a pushed app branch never changes what a pipeline
|
|
81
|
+
# does. See ci/jenkins-lib/.
|
|
82
|
+
globalLibraries:
|
|
83
|
+
libraries:
|
|
84
|
+
- name: "platform"
|
|
85
|
+
defaultVersion: "${PLATFORM_REF}"
|
|
86
|
+
allowVersionOverride: false
|
|
87
|
+
implicit: false
|
|
88
|
+
retriever:
|
|
89
|
+
modernSCM:
|
|
90
|
+
libraryPath: "ci/jenkins-lib/"
|
|
91
|
+
scm:
|
|
92
|
+
git:
|
|
93
|
+
remote: "${JENKINS_GIT_REPO}"
|
|
94
|
+
credentialsId: "git-credentials"
|
|
95
|
+
location:
|
|
96
|
+
url: "https://jenkins.${DOMAIN:-localhost}/"
|
|
97
|
+
# Webhook lands on /github-webhook/. Hooks are registered by hand in the
|
|
98
|
+
# repo settings, not managed from here.
|
|
99
|
+
gitHubPluginConfig:
|
|
100
|
+
configs:
|
|
101
|
+
- name: "GitHub"
|
|
102
|
+
apiUrl: "https://api.github.com"
|
|
103
|
+
credentialsId: "github-token"
|
|
104
|
+
manageHooks: false
|
|
105
|
+
hookSecretConfigs:
|
|
106
|
+
- credentialsId: "github-webhook-secret"
|
|
107
|
+
|
|
108
|
+
# Platform jobs run pipelines from PLATFORM_REF of this repo, never from a pushed branch.
|
|
109
|
+
jobs:
|
|
110
|
+
- script: |
|
|
111
|
+
def fromPlatform = { String path ->
|
|
112
|
+
return {
|
|
113
|
+
cpsScm {
|
|
114
|
+
scm {
|
|
115
|
+
git {
|
|
116
|
+
remote {
|
|
117
|
+
url("${JENKINS_GIT_REPO}")
|
|
118
|
+
credentials("git-credentials")
|
|
119
|
+
}
|
|
120
|
+
branch("${PLATFORM_REF}")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
scriptPath(path)
|
|
124
|
+
lightweight(false)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pipelineJob("platform-registry-gc") {
|
|
130
|
+
displayName("Platform Registry GC")
|
|
131
|
+
description("Nightly: deletes registry images no recent release of any app references, then prunes the BuildKit cache and host images. See ci/registry-gc.sh.")
|
|
132
|
+
definition(fromPlatform("ci/registry-gc.Jenkinsfile"))
|
|
133
|
+
triggers {
|
|
134
|
+
cron("H 3 * * *")
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pipelineJob("rollback") {
|
|
139
|
+
displayName("Rollback")
|
|
140
|
+
description("Redeploys an earlier release of an app: pick the app, pick a release, done. Hands the SHA to <app>/deploy.")
|
|
141
|
+
definition(fromPlatform("ci/rollback.Jenkinsfile"))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ("${PLATFORM_RENOVATE:-false}" == "true") {
|
|
145
|
+
pipelineJob("platform-renovate") {
|
|
146
|
+
displayName("Platform Renovate")
|
|
147
|
+
description("On renovate_schedule (weekly by default): opens PRs that bump pinned versions. Config: renovate.json. See ci/renovate.Jenkinsfile.")
|
|
148
|
+
// Declared here as well: a JCasC reload regenerates the job and drops
|
|
149
|
+
// parameters only the pipeline declares.
|
|
150
|
+
parameters {
|
|
151
|
+
booleanParam("DRY_RUN", false, "Log what would change, open no PRs")
|
|
152
|
+
}
|
|
153
|
+
definition(fromPlatform("ci/renovate.Jenkinsfile"))
|
|
154
|
+
triggers {
|
|
155
|
+
cron("${RENOVATE_SCHEDULE:-H 4 * * 1}")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|