@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -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 +74 -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 +292 -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,30 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # app-remove <app> — take an app off this server, keeping its data.
4
+ #
5
+ # Stops its containers and removes everything the platform set up for it:
6
+ # JCasC fragment, secret files, .env, the Jenkins folder with its jobs and
7
+ # history, the release records (so Rollback no longer offers it). Its images
8
+ # are collected by the next registry GC, since the app is no longer registered.
9
+ #
10
+ # Volumes stay: dropping a line from server.yml must never silently destroy a
11
+ # database. `app-purge <app>` deletes them deliberately.
12
+ #
13
+ # Run by `kayra apps` for every registered app missing from server.yml.
14
+ # Installed by the platform role at <platform_dir>/bin. Run as root.
15
+ # =============================================================================
16
+ set -euo pipefail
17
+
18
+ app="${1:?Usage: app-remove <app>}"
19
+ platform_dir="$(cd "$(dirname "$0")/.." && pwd)"
20
+ apps_dir="$(sed -n 's/^APPS_DIR=//p' "${platform_dir}/.env")"
21
+
22
+ docker compose -p "$app" down --remove-orphans
23
+ rm -rf "${platform_dir}/jenkins/casc/10-${app}.yaml" \
24
+ "${platform_dir}/jenkins-secrets/${app}" \
25
+ "${apps_dir:?}/${app}"
26
+ # Without its fragment the reload drops the app's credentials.
27
+ "${platform_dir}/bin/jenkins-reload"
28
+ "${platform_dir}/bin/jenkins-api" POST "/job/${app}/doDelete" -o /dev/null || true
29
+ (cd "$platform_dir" && docker compose exec -T jenkins-controller rm -rf "/var/jenkins_home/releases/${app}")
30
+ echo "Removed ${app}. Its volumes are kept — app-purge ${app} deletes them."
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # jenkins-api <METHOD> <path> [curl args…] — one authenticated call to the
4
+ # Jenkins API, from inside the controller (admin credentials from its env,
5
+ # CSRF crumb fetched in the same session). Prints the response body.
6
+ #
7
+ # jenkins-api GET '/api/json?tree=jobs[name]'
8
+ # jenkins-api POST /configuration-as-code/reload
9
+ #
10
+ # Installed by the platform role at <platform_dir>/bin.
11
+ # =============================================================================
12
+ set -euo pipefail
13
+
14
+ method="${1:?Usage: jenkins-api <METHOD> <path> [curl args…]}"
15
+ path="${2:?}"
16
+ shift 2
17
+
18
+ cd "$(dirname "$0")/.."
19
+ docker compose exec -T jenkins-controller sh -ec '
20
+ jar=$(mktemp)
21
+ trap "rm -f $jar" EXIT
22
+ auth="${JENKINS_ADMIN_USER}:${JENKINS_ADMIN_PASSWORD}"
23
+ base=http://127.0.0.1:8080
24
+ crumb=$(curl -sf -c "$jar" -u "$auth" \
25
+ "$base/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)")
26
+ method="$1"; path="$2"; shift 2
27
+ curl -sf -b "$jar" -u "$auth" -H "$crumb" -X "$method" "$base$path" "$@"
28
+ ' sh "$method" "$path" "$@"
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # jenkins-app-apply <app> — the one way an app registers with Jenkins.
4
+ #
5
+ # Reads the app's secrets as KEY=VALUE lines on stdin, writes each to
6
+ # jenkins-secrets/<app>/<KEY> (where its 10-<app>.yaml reads them via
7
+ # ${readFile:/run/app-secrets/<app>/<KEY>}), then runs jenkins-reload.
8
+ #
9
+ # Installed by the platform role at <platform_dir>/bin. Run as root.
10
+ # =============================================================================
11
+ set -euo pipefail
12
+
13
+ app="${1:?Usage: jenkins-app-apply <app> < secrets.env}"
14
+ platform_dir="$(cd "$(dirname "$0")/.." && pwd)"
15
+ secrets_dir="${platform_dir}/jenkins-secrets/${app}"
16
+
17
+ # Staged next to the target and swapped in whole, so keys the app no longer
18
+ # declares disappear instead of lingering.
19
+ staging="${secrets_dir}.new"
20
+ rm -rf "$staging"
21
+ mkdir -p "$staging"
22
+ trap 'rm -rf "$staging"' EXIT
23
+ while IFS= read -r line || [ -n "$line" ]; do
24
+ case "$line" in ''|'#'*) continue ;; esac
25
+ printf '%s' "${line#*=}" > "${staging}/${line%%=*}"
26
+ done
27
+ rm -rf "$secrets_dir"
28
+ mv "$staging" "$secrets_dir"
29
+ trap - EXIT
30
+ # The controller runs as uid 1000 and reads these through a read-only mount.
31
+ chown -R 1000:1000 "$secrets_dir"
32
+ find "$secrets_dir" -type f -exec chmod 0400 {} +
33
+ chmod 0500 "$secrets_dir"
34
+
35
+ "${platform_dir}/bin/jenkins-reload"
36
+ echo "Applied Jenkins config for ${app}."
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # jenkins-reload — re-apply every JCasC fragment in jenkins/casc/ in place.
4
+ #
5
+ # Reload, not restart: Jenkins is shared, and recreating the controller for one
6
+ # config change would kill every app's running builds.
7
+ #
8
+ # Installed by the platform role at <platform_dir>/bin.
9
+ # =============================================================================
10
+ set -euo pipefail
11
+
12
+ "$(dirname "$0")/jenkins-api" POST /configuration-as-code/reload -o /dev/null
@@ -0,0 +1,54 @@
1
+ # =============================================================================
2
+ # Hetzner Cloud Firewall Module
3
+ # Creates a firewall with standard web server rules.
4
+ # =============================================================================
5
+
6
+ terraform {
7
+ required_providers {
8
+ hcloud = {
9
+ source = "hetznercloud/hcloud"
10
+ version = "~> 1.45"
11
+ }
12
+ }
13
+ }
14
+
15
+ resource "hcloud_firewall" "this" {
16
+ name = var.name
17
+
18
+ # SSH
19
+ rule {
20
+ direction = "in"
21
+ protocol = "tcp"
22
+ port = "22"
23
+ source_ips = var.ssh_allowed_ips
24
+ }
25
+
26
+ # HTTP
27
+ rule {
28
+ direction = "in"
29
+ protocol = "tcp"
30
+ port = "80"
31
+ source_ips = ["0.0.0.0/0", "::/0"]
32
+ }
33
+
34
+ # HTTPS
35
+ rule {
36
+ direction = "in"
37
+ protocol = "tcp"
38
+ port = "443"
39
+ source_ips = ["0.0.0.0/0", "::/0"]
40
+ }
41
+
42
+ # Anything else the server's kind needs open to the world (e.g. WireGuard).
43
+ dynamic "rule" {
44
+ for_each = var.extra_rules
45
+ content {
46
+ direction = "in"
47
+ protocol = rule.value.protocol
48
+ port = rule.value.port
49
+ source_ips = ["0.0.0.0/0", "::/0"]
50
+ }
51
+ }
52
+
53
+ labels = var.labels
54
+ }
@@ -0,0 +1,9 @@
1
+ output "firewall_id" {
2
+ description = "Firewall ID"
3
+ value = hcloud_firewall.this.id
4
+ }
5
+
6
+ output "firewall_name" {
7
+ description = "Firewall name"
8
+ value = hcloud_firewall.this.name
9
+ }
@@ -0,0 +1,22 @@
1
+ variable "name" {
2
+ description = "Firewall name"
3
+ type = string
4
+ }
5
+
6
+ variable "ssh_allowed_ips" {
7
+ description = "CIDR ranges allowed to SSH (default: anywhere)"
8
+ type = list(string)
9
+ default = ["0.0.0.0/0", "::/0"]
10
+ }
11
+
12
+ variable "extra_rules" {
13
+ description = "Additional inbound rules open to everyone, e.g. [{protocol = \"udp\", port = \"51820\"}]"
14
+ type = list(object({ protocol = string, port = string }))
15
+ default = []
16
+ }
17
+
18
+ variable "labels" {
19
+ description = "Labels to attach to the firewall"
20
+ type = map(string)
21
+ default = {}
22
+ }
@@ -0,0 +1,62 @@
1
+ # =============================================================================
2
+ # Hetzner Cloud Server Module
3
+ # Provisions a VPS with SSH key and cloud-init.
4
+ # =============================================================================
5
+
6
+ terraform {
7
+ required_providers {
8
+ hcloud = {
9
+ source = "hetznercloud/hcloud"
10
+ version = "~> 1.45"
11
+ }
12
+ }
13
+ }
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # SSH key
17
+ # ---------------------------------------------------------------------------
18
+ resource "hcloud_ssh_key" "deploy" {
19
+ name = "${var.name}-deploy"
20
+ public_key = var.ssh_public_key
21
+ }
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Server
25
+ # ---------------------------------------------------------------------------
26
+ resource "hcloud_server" "this" {
27
+ name = var.name
28
+ server_type = var.server_type
29
+ image = var.image
30
+ location = var.location
31
+ ssh_keys = [hcloud_ssh_key.deploy.id]
32
+
33
+ firewall_ids = var.firewall_ids
34
+
35
+ user_data = templatefile("${path.module}/../../../cloud-init.yml.tpl", {
36
+ hostname = var.name
37
+ ssh_pubkey = var.ssh_public_key
38
+ })
39
+
40
+ labels = var.labels
41
+
42
+ # cloud-init runs once, at creation, and Hetzner replaces the server for any
43
+ # change to these. Editing the template (or adopting a server that was never
44
+ # created by us) must not destroy it; Ansible converges existing servers.
45
+ lifecycle {
46
+ ignore_changes = [user_data, ssh_keys, image]
47
+ }
48
+ }
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Ansible inventory file
52
+ # ---------------------------------------------------------------------------
53
+ resource "local_file" "ansible_inventory" {
54
+ content = templatefile("${path.module}/templates/inventory.ini.tpl", {
55
+ hostname = var.name
56
+ ssh_host = hcloud_server.this.ipv4_address
57
+ ssh_port = 22
58
+ ssh_user = "deploy"
59
+ ssh_key_path = var.ssh_private_key_path
60
+ })
61
+ filename = var.inventory_path
62
+ }
@@ -0,0 +1,19 @@
1
+ output "ipv4_address" {
2
+ description = "Public IPv4 address of the server"
3
+ value = hcloud_server.this.ipv4_address
4
+ }
5
+
6
+ output "server_id" {
7
+ description = "Hetzner server ID"
8
+ value = hcloud_server.this.id
9
+ }
10
+
11
+ output "server_name" {
12
+ description = "Server name"
13
+ value = hcloud_server.this.name
14
+ }
15
+
16
+ output "inventory_path" {
17
+ description = "Path to the generated Ansible inventory"
18
+ value = local_file.ansible_inventory.filename
19
+ }
@@ -0,0 +1,5 @@
1
+ [servers]
2
+ ${hostname} ansible_host=${ssh_host} ansible_port=${ssh_port} ansible_user=${ssh_user} ansible_ssh_private_key_file=${ssh_key_path}
3
+
4
+ [servers:vars]
5
+ ansible_ssh_common_args=-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
@@ -0,0 +1,50 @@
1
+ variable "name" {
2
+ description = "Server name"
3
+ type = string
4
+ }
5
+
6
+ variable "server_type" {
7
+ description = "Hetzner server type (e.g., cx22, cx32)"
8
+ type = string
9
+ default = "cx22"
10
+ }
11
+
12
+ variable "image" {
13
+ description = "OS image"
14
+ type = string
15
+ default = "ubuntu-24.04"
16
+ }
17
+
18
+ variable "location" {
19
+ description = "Hetzner datacenter location"
20
+ type = string
21
+ default = "nbg1"
22
+ }
23
+
24
+ variable "ssh_public_key" {
25
+ description = "SSH public key for the deploy user"
26
+ type = string
27
+ }
28
+
29
+ variable "ssh_private_key_path" {
30
+ description = "Path to the SSH private key (for inventory generation)"
31
+ type = string
32
+ }
33
+
34
+ variable "firewall_ids" {
35
+ description = "List of firewall IDs to attach"
36
+ type = list(number)
37
+ default = []
38
+ }
39
+
40
+ variable "labels" {
41
+ description = "Labels to attach to the server"
42
+ type = map(string)
43
+ default = {}
44
+ }
45
+
46
+ variable "inventory_path" {
47
+ description = "Path to write the Ansible inventory file"
48
+ type = string
49
+ default = "./inventory.ini"
50
+ }
@@ -0,0 +1,67 @@
1
+ # =============================================================================
2
+ # Multipass Instance Module
3
+ # Provisions a local VM via Multipass with the same cloud-init as production.
4
+ # Generates a dedicated, disposable SSH keypair for the instance — unlike
5
+ # the hcloud-server module, which expects the operator's own persistent key.
6
+ # =============================================================================
7
+
8
+ terraform {
9
+ required_providers {
10
+ multipass = {
11
+ source = "todoroff/multipass"
12
+ }
13
+ tls = {
14
+ source = "hashicorp/tls"
15
+ }
16
+ local = {
17
+ source = "hashicorp/local"
18
+ }
19
+ }
20
+ }
21
+
22
+ resource "tls_private_key" "deploy" {
23
+ algorithm = "ED25519"
24
+ }
25
+
26
+ resource "local_sensitive_file" "private_key" {
27
+ content = tls_private_key.deploy.private_key_openssh
28
+ filename = "${var.ssh_key_dir}/${var.name}_ed25519"
29
+ file_permission = "0600"
30
+ }
31
+
32
+ resource "local_file" "public_key" {
33
+ content = tls_private_key.deploy.public_key_openssh
34
+ filename = "${var.ssh_key_dir}/${var.name}_ed25519.pub"
35
+ }
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Instance
39
+ # ---------------------------------------------------------------------------
40
+ resource "multipass_instance" "this" {
41
+ name = var.name
42
+ image = var.image
43
+ cpus = var.cpus
44
+ memory = var.memory
45
+ disk = var.disk
46
+
47
+ cloud_init = templatefile("${path.module}/../../../cloud-init.yml.tpl", {
48
+ hostname = var.name
49
+ ssh_pubkey = tls_private_key.deploy.public_key_openssh
50
+ })
51
+
52
+ wait_for_cloud_init = true
53
+ }
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Ansible inventory file
57
+ # ---------------------------------------------------------------------------
58
+ resource "local_file" "ansible_inventory" {
59
+ content = templatefile("${path.module}/templates/inventory.ini.tpl", {
60
+ hostname = var.name
61
+ ssh_host = multipass_instance.this.ipv4[0]
62
+ ssh_port = 22
63
+ ssh_user = "deploy"
64
+ ssh_key_path = local_sensitive_file.private_key.filename
65
+ })
66
+ filename = var.inventory_path
67
+ }
@@ -0,0 +1,19 @@
1
+ output "ipv4_address" {
2
+ description = "Primary IPv4 address of the instance"
3
+ value = multipass_instance.this.ipv4[0]
4
+ }
5
+
6
+ output "instance_name" {
7
+ description = "Multipass instance name"
8
+ value = multipass_instance.this.name
9
+ }
10
+
11
+ output "inventory_path" {
12
+ description = "Path to the generated Ansible inventory"
13
+ value = local_file.ansible_inventory.filename
14
+ }
15
+
16
+ output "ssh_private_key_path" {
17
+ description = "Path to the generated SSH private key"
18
+ value = local_sensitive_file.private_key.filename
19
+ }
@@ -0,0 +1,5 @@
1
+ [servers]
2
+ ${hostname} ansible_host=${ssh_host} ansible_port=${ssh_port} ansible_user=${ssh_user} ansible_ssh_private_key_file=${ssh_key_path}
3
+
4
+ [servers:vars]
5
+ ansible_ssh_common_args=-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
@@ -0,0 +1,39 @@
1
+ variable "name" {
2
+ description = "Instance name"
3
+ type = string
4
+ }
5
+
6
+ variable "image" {
7
+ description = "Multipass image alias"
8
+ type = string
9
+ default = "24.04"
10
+ }
11
+
12
+ variable "cpus" {
13
+ description = "Virtual CPU count"
14
+ type = number
15
+ default = 2
16
+ }
17
+
18
+ variable "memory" {
19
+ description = "Memory size (e.g. 4G)"
20
+ type = string
21
+ default = "2G"
22
+ }
23
+
24
+ variable "disk" {
25
+ description = "Disk size (e.g. 20G)"
26
+ type = string
27
+ default = "10G"
28
+ }
29
+
30
+ variable "ssh_key_dir" {
31
+ description = "Directory to write the generated SSH keypair into"
32
+ type = string
33
+ }
34
+
35
+ variable "inventory_path" {
36
+ description = "Path to write the Ansible inventory file"
37
+ type = string
38
+ default = "./inventory.ini"
39
+ }