@yunuseon/kayra 0.0.0-stage → 0.5.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -2
  3. package/ansible/.ansible-lint +13 -0
  4. package/ansible/ansible.cfg +12 -0
  5. package/ansible/playbooks/adopt.yml +35 -0
  6. package/ansible/playbooks/app.yml +21 -0
  7. package/ansible/playbooks/apps-prune.yml +30 -0
  8. package/ansible/playbooks/edge.yml +41 -0
  9. package/ansible/playbooks/nas.yml +47 -0
  10. package/ansible/playbooks/platform.yml +62 -0
  11. package/ansible/requirements.yml +7 -0
  12. package/ansible/roles/app/defaults/main.yml +9 -0
  13. package/ansible/roles/app/tasks/main.yml +107 -0
  14. package/ansible/roles/app/templates/app.env.j2 +9 -0
  15. package/ansible/roles/app/templates/casc-app.yaml.j2 +68 -0
  16. package/ansible/roles/backup-check/files/backup-check.service +9 -0
  17. package/ansible/roles/backup-check/files/backup-check.timer +10 -0
  18. package/ansible/roles/backup-check/tasks/main.yml +59 -0
  19. package/ansible/roles/backup-check/templates/backup-check.sh.j2 +117 -0
  20. package/ansible/roles/common/handlers/main.yml +16 -0
  21. package/ansible/roles/common/tasks/main.yml +100 -0
  22. package/ansible/roles/compose-stacks/tasks/main.yml +102 -0
  23. package/ansible/roles/docker/defaults/main.yml +3 -0
  24. package/ansible/roles/docker/files/buildkit-rootless +23 -0
  25. package/ansible/roles/docker/handlers/main.yml +12 -0
  26. package/ansible/roles/docker/tasks/main.yml +82 -0
  27. package/ansible/roles/edge-proxy/files/docker-compose.yml +13 -0
  28. package/ansible/roles/edge-proxy/handlers/main.yml +6 -0
  29. package/ansible/roles/edge-proxy/tasks/main.yml +34 -0
  30. package/ansible/roles/edge-proxy/templates/Caddyfile.j2 +25 -0
  31. package/ansible/roles/forgejo-config/tasks/main.yml +333 -0
  32. package/ansible/roles/platform/defaults/main.yml +76 -0
  33. package/ansible/roles/platform/tasks/main.yml +298 -0
  34. package/ansible/roles/platform/templates/authelia-users.yml.j2 +17 -0
  35. package/ansible/roles/platform/templates/platform.env.j2 +41 -0
  36. package/ansible/roles/restic-backup/files/docker-compose.postgres.yml +27 -0
  37. package/ansible/roles/restic-backup/files/docker-compose.yml +12 -0
  38. package/ansible/roles/restic-backup/handlers/main.yml +7 -0
  39. package/ansible/roles/restic-backup/tasks/main.yml +121 -0
  40. package/ansible/roles/restic-backup/templates/backup.env.j2 +9 -0
  41. package/ansible/roles/restic-backup/templates/backup.sh.j2 +28 -0
  42. package/ansible/roles/restic-backup/templates/docker-compose.override.yml.j2 +26 -0
  43. package/ansible/roles/restic-backup/templates/follow-restore-test.sh.j2 +28 -0
  44. package/ansible/roles/restic-backup/templates/restore-test.sh.j2 +51 -0
  45. package/ansible/roles/restic-backup/templates/root.crontab.j2 +6 -0
  46. package/ansible/roles/restic-backup/templates/ssh_config.j2 +12 -0
  47. package/ansible/roles/wireguard/handlers/main.yml +9 -0
  48. package/ansible/roles/wireguard/tasks/main.yml +33 -0
  49. package/ansible/roles/wireguard/templates/wg0.conf.j2 +12 -0
  50. package/bin/kayra.js +29 -0
  51. package/cli/dist/brand.js +41 -0
  52. package/cli/dist/cli.js +21 -0
  53. package/cli/dist/commands/doctor.js +79 -0
  54. package/cli/dist/commands/server.js +215 -0
  55. package/cli/dist/commands/status.js +43 -0
  56. package/cli/dist/fleet.js +134 -0
  57. package/cli/dist/interpret.js +98 -0
  58. package/cli/dist/load.js +49 -0
  59. package/cli/dist/main.js +94 -0
  60. package/cli/dist/plan.js +31 -0
  61. package/cli/dist/result.js +5 -0
  62. package/cloud-init.yml.tpl +14 -0
  63. package/compose/authelia/configuration.yml +138 -0
  64. package/compose/buildkit/buildkitd.toml +14 -0
  65. package/compose/docker-compose.acme-dns.yml +12 -0
  66. package/compose/docker-compose.ci.yml +156 -0
  67. package/compose/docker-compose.yml +83 -0
  68. package/compose/jenkins/Dockerfile +40 -0
  69. package/compose/jenkins/casc/00-platform.yaml +158 -0
  70. package/compose/jenkins/plugins.txt +97 -0
  71. package/compose/jenkins-agent/Dockerfile +24 -0
  72. package/compose/traefik/dynamic/authelia.yml +18 -0
  73. package/compose/traefik/dynamic/middlewares.yml +14 -0
  74. package/compose/traefik/dynamic/ratelimit.yml +21 -0
  75. package/compose/traefik/dynamic/tls.yml +9 -0
  76. package/compose/traefik/traefik.yml +46 -0
  77. package/hosts.conf +6 -0
  78. package/package.json +73 -4
  79. package/scripts/local/app-manifests.sh +26 -0
  80. package/scripts/local/authelia-dev-init.sh +58 -0
  81. package/scripts/local/certs.sh +55 -0
  82. package/scripts/local/home-image.sh +55 -0
  83. package/scripts/local/home-reach.sh +41 -0
  84. package/scripts/local/require-main.sh +38 -0
  85. package/scripts/local/setup-hosts.sh +30 -0
  86. package/scripts/server/app-purge +26 -0
  87. package/scripts/server/app-remove +30 -0
  88. package/scripts/server/jenkins-api +28 -0
  89. package/scripts/server/jenkins-app-apply +36 -0
  90. package/scripts/server/jenkins-reload +12 -0
  91. package/terraform/modules/hcloud-firewall/main.tf +54 -0
  92. package/terraform/modules/hcloud-firewall/outputs.tf +9 -0
  93. package/terraform/modules/hcloud-firewall/variables.tf +22 -0
  94. package/terraform/modules/hcloud-server/main.tf +62 -0
  95. package/terraform/modules/hcloud-server/outputs.tf +19 -0
  96. package/terraform/modules/hcloud-server/templates/inventory.ini.tpl +5 -0
  97. package/terraform/modules/hcloud-server/variables.tf +50 -0
  98. package/terraform/modules/multipass-instance/main.tf +67 -0
  99. package/terraform/modules/multipass-instance/outputs.tf +19 -0
  100. package/terraform/modules/multipass-instance/templates/inventory.ini.tpl +5 -0
  101. package/terraform/modules/multipass-instance/variables.tf +39 -0
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ # Every plugin, dependencies included, at the version prod runs: without the
2
+ # dependencies pinned, jenkins-plugin-cli resolves them to whatever is newest
3
+ # on the day the image is built. Renovate proposes updates.
4
+ antisamy-markup-formatter:173.v680e3a_b_69ff3
5
+ apache-httpcomponents-client-4-api:4.5.14-269.vfa_2321039a_83
6
+ apache-httpcomponents-client-5-api:5.6.4-207.vd5b_a_c15e4de5
7
+ asm-api:9.10.1-216.va_9256d3b_844b_
8
+ authentication-tokens:1.144.v5ff4a_5ec5c33
9
+ bootstrap5-api:5.3.8-1048.va_c299057e35c
10
+ bouncycastle-api:2.30.1.86.1-306.vfc711fa_36d5f
11
+ branch-api:2.1303.v9f3b_95dc329d
12
+ caffeine-api:3.3.0-210.v71b_2985ce530
13
+ checks-api:422.vc7e8b_51488c6
14
+ cloud-stats:423.v63f017a_f582c
15
+ cloudbees-folder:6.1106.v3a_d9a_6d2465e
16
+ commons-compress-api:1.28.0-95.vb_e5e24f83645
17
+ commons-lang3-api:3.21.0-122.v6a_ea_4fd34049
18
+ commons-text-api:1.15.0-218.va_61573470393
19
+ configuration-as-code:2133.ve570217cdb_cd
20
+ credentials-binding:728.v902a_273b_8947
21
+ credentials:1511.v2e3cb_0008ef0
22
+ display-url-api:2.217.va_6b_de84cc74b_
23
+ docker-commons:477.v289085a_b_6896
24
+ docker-java-api:3.7.1-136.v5d70f77a_c3d6
25
+ docker-plugin:1331.v2c036a_dc63fc
26
+ docker-workflow:653.v2f2c08eff0ec
27
+ durable-task:686.v80ff80875b_82
28
+ echarts-api:6.1.0-1321.v86c4c5114494
29
+ eddsa-api:0.3.0.1-29.v67e9a_1c969b_b_
30
+ font-awesome-api:7.3.1-1013.v0835a_879ec6d
31
+ git-client:6.6.1
32
+ git:5.10.1
33
+ github-api:1.330-492.v3941a_032db_2a_
34
+ github:1.47.0
35
+ gson-api:2.14.0-201.v8eefe5515533
36
+ instance-identity:203.v15e81a_1b_7a_38
37
+ ionicons-api:94.vcc3065403257
38
+ jackson-annotations2-api:2.22-19.v10a_a_582ea_26e
39
+ jackson2-api:2.22.3-447.v6a_93ce7d2c82
40
+ jackson3-api:3.2.3-101.v69fc84453a_8e
41
+ jakarta-activation-api:2.1.4-1
42
+ jakarta-mail-api:2.1.5-1
43
+ jakarta-xml-bind-api:4.0.9-19.v2b_a_5b_44d9a_1c
44
+ javax-activation-api:1.2.0-8
45
+ jaxb:2.3.9-143.v5979df3304e6
46
+ job-dsl:3732.v9a_c49a_61a_313
47
+ joda-time-api:2.15.0-208.v6b_084e7a_d9f0
48
+ jquery3-api:3.7.1-687.v68d468e40b_30
49
+ json-api:20260814-226.v20f9685d642c
50
+ json-path-api:3.0.0-218.vcd4dd1355de2
51
+ junit:1434.vde2d24df0c4c
52
+ mailer:534.v1b_36f5864073
53
+ matrix-project:928.v1effc3f3a_236
54
+ mina-sshd-api-common:2.19.0-192.v2b_a_7b_2c1dc71
55
+ mina-sshd-api-core:2.19.0-192.v2b_a_7b_2c1dc71
56
+ okhttp-api:5.3.2-200.vedb_720a_cf1f8
57
+ pipeline-build-step:601.v6d4c6d1a_9dc7
58
+ pipeline-graph-analysis:254.v0f63a_a_447dca_
59
+ pipeline-graph-view:1053.v9df19e6ceed2
60
+ pipeline-groovy-lib:806.v408277b_33d1d
61
+ pipeline-input-step:560.v56198a_642157
62
+ pipeline-milestone-step:152.v6e22b_8cfc66c
63
+ pipeline-model-api:2.2293.v6e7193cec599
64
+ pipeline-model-definition:2.2293.v6e7193cec599
65
+ pipeline-model-extensions:2.2293.v6e7193cec599
66
+ pipeline-rest-api:2.41
67
+ pipeline-stage-step:345.va_96187909426
68
+ pipeline-stage-tags-metadata:2.2293.v6e7193cec599
69
+ pipeline-stage-view:2.41
70
+ pipeline-utility-steps:3.810.va_7672d206740
71
+ plain-credentials:199.v9f8e1f741799
72
+ plugin-util-api:7.1341.v039f146993d9
73
+ prism-api:1.30.0-741.v034eb_0b_0a_a_fa_
74
+ resource-disposer:0.25
75
+ scm-api:728.vc30dcf7a_0df5
76
+ script-security:1433.v17c58a_e45cc1
77
+ snakeyaml-api:2.7-152.vc672728cf536
78
+ snakeyaml-engine-api:3.2-18.v885ed7ff698f
79
+ ssh-credentials:372.va_250881b_08cd
80
+ ssh-slaves:3.1097.v868116049892
81
+ structs:362.va_b_695ef4fdf9
82
+ timestamper:1.30
83
+ token-macro:477.vd4f0dc3cb_cf1
84
+ trilead-api:2.284.v1974ea_324382
85
+ variant:70.va_d9f17f859e0
86
+ woodstox-core-api:7.3.0-16.v017f30ed004c
87
+ workflow-aggregator:608.v67378e9d3db_1
88
+ workflow-api:1413.v2ff1a_5e720fa_
89
+ workflow-basic-steps:1098.v808b_fd7f8cf4
90
+ workflow-cps:4391.v652b_b_b_1b_7c00
91
+ workflow-durable-task-step:1479.v56e587f413a_7
92
+ workflow-job:1609.v8926e7962024
93
+ workflow-multibranch:842.v3a_b_59b_57b_e6e
94
+ workflow-scm-step:487.v41313a_96b_65d
95
+ workflow-step-api:724.v538c2362b_dfb_
96
+ workflow-support:1015.v785e5a_b_b_8b_22
97
+ ws-cleanup:0.49
@@ -0,0 +1,24 @@
1
+ # =============================================================================
2
+ # Jenkins Inbound Agent — git + buildctl + jq, nothing app-specific.
3
+ #
4
+ # Deliberately has NO docker CLI and no docker socket: it runs code from
5
+ # arbitrary branches, and the host's socket would make that host root. Images
6
+ # are built by buildkitd over TCP. No language toolchain either: apps build and
7
+ # test inside their Dockerfiles, so a new app never changes this image.
8
+ # =============================================================================
9
+
10
+ FROM jenkins/inbound-agent:3386.v353e57a_1b_ea_0-1-jdk21
11
+
12
+ USER root
13
+
14
+ # renovate: datasource=github-releases depName=moby/buildkit
15
+ ARG BUILDKIT_VERSION=v0.34.0
16
+
17
+ RUN apt-get update && \
18
+ apt-get install -y --no-install-recommends ca-certificates curl jq && \
19
+ arch="$(dpkg --print-architecture)" && \
20
+ curl -fsSL "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/buildkit-${BUILDKIT_VERSION}.linux-${arch}.tar.gz" \
21
+ | tar -xz -C /usr/local bin/buildctl && \
22
+ rm -rf /var/lib/apt/lists/*
23
+
24
+ USER jenkins
@@ -0,0 +1,18 @@
1
+ # Dynamic Traefik configuration — Authelia forward authentication.
2
+ # Referenced from router labels as `authelia@file`.
3
+ http:
4
+ middlewares:
5
+ authelia:
6
+ forwardAuth:
7
+ address: 'http://authelia:9091/api/authz/forward-auth'
8
+ # Safe because neither entrypoint in traefik.yml sets
9
+ # forwardedHeaders.trustedIPs, so Traefik overwrites any client-supplied
10
+ # X-Forwarded-* before this middleware runs. Do not add trustedIPs.
11
+ trustForwardHeader: true
12
+ # The regex form strips every matching header from the inbound request
13
+ # before copying Authelia's back, closing the gap where Authelia
14
+ # returns no value for one of them and a client-supplied one survives.
15
+ authResponseHeadersRegex: '^Remote-(User|Groups|Email|Name)$'
16
+ # Traefik warns on every request without this. Authelia's auth
17
+ # responses are small; an unbounded body is a memory-exhaustion vector.
18
+ maxResponseBodySize: 8192
@@ -0,0 +1,14 @@
1
+ # Dynamic Traefik configuration — middlewares
2
+ http:
3
+ middlewares:
4
+ # Security headers for all services
5
+ security-headers:
6
+ headers:
7
+ browserXssFilter: true
8
+ contentTypeNosniff: true
9
+ frameDeny: true
10
+ sslRedirect: false
11
+ customFrameOptionsValue: "SAMEORIGIN"
12
+ stsSeconds: 31536000
13
+ stsIncludeSubdomains: true
14
+ referrerPolicy: "strict-origin-when-cross-origin"
@@ -0,0 +1,21 @@
1
+ # Dynamic Traefik configuration — rate limiting.
2
+ http:
3
+ middlewares:
4
+ # The trial-request endpoint is public and unauthenticated by design: it is
5
+ # how strangers reach the club. That also makes it the one spam vector in
6
+ # the system, so it gets a ceiling.
7
+ #
8
+ # No sourceCriterion: the default keys on the client's remote address,
9
+ # which is correct when Traefik is the edge. An ipStrategy.depth here would
10
+ # expect an X-Forwarded-For chain that does not exist for a direct client,
11
+ # and the limit silently never engages.
12
+ #
13
+ # Values are empirical, not arithmetic: shorter periods did not actually
14
+ # throttle in testing, this regime does. 20 per 10 minutes with a burst of
15
+ # 5 is far more than a human filling in a form once, and caps a spammer at
16
+ # roughly 3k/day.
17
+ trial-ratelimit:
18
+ rateLimit:
19
+ average: 20
20
+ period: "10m"
21
+ burst: 5
@@ -0,0 +1,9 @@
1
+ # Dynamic TLS — file-based certificates for local + VM development.
2
+ # Generated by `make certs [VM_DOMAIN=...]` (mkcert under the hood).
3
+ # In prod, this directory is empty and ACME (Let's Encrypt) handles certs
4
+ # automatically. Traefik logs a warning for missing files but routes still
5
+ # work via the entrypoint's default certResolver.
6
+ tls:
7
+ certificates:
8
+ - certFile: /certs/cert.pem
9
+ keyFile: /certs/key.pem
@@ -0,0 +1,46 @@
1
+ # Traefik static configuration
2
+ api:
3
+ dashboard: true
4
+ # No insecure listener: the dashboard is served through a normal router on
5
+ # traefik.<DOMAIN>, behind Authelia. See docker-compose.yml.
6
+ insecure: false
7
+
8
+ entryPoints:
9
+ web:
10
+ address: ":80"
11
+ http:
12
+ redirections:
13
+ entryPoint:
14
+ to: websecure
15
+ scheme: https
16
+ permanent: true
17
+ websecure:
18
+ address: ":443"
19
+ http:
20
+ tls:
21
+ certResolver: le
22
+ # Every router, apps included: HSTS on half the hosts protects nothing.
23
+ middlewares:
24
+ - security-headers@file
25
+
26
+ certificatesResolvers:
27
+ le:
28
+ acme:
29
+ # Traefik does not expand env vars in its static file; the platform role
30
+ # substitutes acme_email on copy. Local dev never registers.
31
+ email: "acme@example.com"
32
+ storage: /acme/acme.json
33
+ httpChallenge:
34
+ entryPoint: web
35
+
36
+ providers:
37
+ docker:
38
+ endpoint: "unix:///var/run/docker.sock"
39
+ exposedByDefault: false
40
+ network: platform-public
41
+ file:
42
+ directory: /etc/traefik/dynamic
43
+ watch: true
44
+
45
+ log:
46
+ level: INFO
package/hosts.conf ADDED
@@ -0,0 +1,6 @@
1
+ # Platform subdomains to add to /etc/hosts and to the mkcert SAN list.
2
+ # App subdomains live in each app's app.yml; apps may not claim these.
3
+ # One subdomain per line. Read by infrastructure/scripts/local/setup-hosts.sh and infrastructure/scripts/local/certs.sh.
4
+ jenkins
5
+ auth
6
+ traefik
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@yunuseon/kayra",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.0",
4
+ "description": "A self-hosted app platform and the CLI that runs it: Traefik, Authelia, Jenkins, a registry and rootless BuildKit per server, set up with OpenTofu and Ansible.",
5
+ "license": "MIT",
6
+ "author": "Yunus Öztürk",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/yunuseon/kayra.git"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "bin": {
16
+ "kayra": "bin/kayra.js"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "cli/dist/",
21
+ "ansible/",
22
+ "compose/",
23
+ "terraform/",
24
+ "scripts/",
25
+ "cloud-init.yml.tpl",
26
+ "hosts.conf"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "scripts": {
33
+ "kayra": "node cli/src/main.ts",
34
+ "build": "tsc -p cli/tsconfig.build.json",
35
+ "prepack": "npm run build",
36
+ "typecheck": "tsc -p cli --noEmit",
37
+ "lint": "cd cli && eslint --flag unstable_native_nodejs_ts_config .",
38
+ "test": "node --test 'cli/test/**/*.test.ts'"
39
+ },
40
+ "dependencies": {
41
+ "@optique/core": "1.3.3",
42
+ "@optique/run": "1.3.3",
43
+ "yaml": "2.9.1"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "10.0.1",
47
+ "@tsconfig/node24": "24.0.5",
48
+ "@tsconfig/strictest": "2.0.8",
49
+ "@types/node": "24.19.1",
50
+ "eslint": "10.12.0",
51
+ "eslint-plugin-functional": "10.0.1",
52
+ "typescript": "6.0.3",
53
+ "typescript-eslint": "8.71.1"
54
+ },
55
+ "overrides": {
56
+ "eslint-plugin-functional": {
57
+ "deepmerge-ts": "8.0.2"
58
+ }
59
+ },
60
+ "homepage": "https://github.com/yunuseon/kayra#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/yunuseon/kayra/issues"
63
+ },
64
+ "keywords": [
65
+ "paas",
66
+ "self-hosted",
67
+ "platform",
68
+ "traefik",
69
+ "jenkins",
70
+ "ansible",
71
+ "opentofu",
72
+ "hetzner",
73
+ "docker-compose"
74
+ ]
75
+ }
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Print the path of every app.yml a server runs, one per line, from the
3
+ # platform's cache of each app's repo (.cache/apps/<name>, the same clone the
4
+ # app role reads). A missing clone is
5
+ # fetched with the operator's own git credentials (kayra doctor checks them).
6
+ #
7
+ # Usage: FLEET_ROOT=<fleet> bash scripts/local/app-manifests.sh [--subdomains] <server-name>
8
+ # --subdomains print the apps' subdomains instead (for certs and /etc/hosts)
9
+ set -euo pipefail
10
+
11
+ FLEET_ROOT="${FLEET_ROOT:?the directory of the fleet (servers/), set by kayra}"
12
+ subdomains=false
13
+ [ "${1:-}" != "--subdomains" ] || { subdomains=true; shift; }
14
+ name="${1:?Usage: app-manifests.sh [--subdomains] <server-name>}"
15
+ descriptor="${FLEET_ROOT}/servers/${name}/server.yml"
16
+
17
+ yq -o json -I0 '.apps // [] | .[]' "$descriptor" | while read -r app; do
18
+ path="$(jq -r '.path // "."' <<< "$app")"
19
+ repo="$(jq -r '.repo' <<< "$app")"
20
+ clone="${FLEET_ROOT}/.cache/apps/$(jq -r '.name' <<< "$app")"
21
+ if [ ! -d "${clone}/.git" ]; then
22
+ git clone -q --depth 1 --branch "$(jq -r '.ref // "main"' <<< "$app")" "$repo" "$clone"
23
+ fi
24
+ manifest="${clone}/${path}/app.yml"
25
+ if [ "$subdomains" = true ]; then yq '.subdomains[]' "$manifest"; else echo "$manifest"; fi
26
+ done