kube_cluster 0.2.0 → 0.2.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/tag-gem-version-bump.yml +47 -0
  4. data/.gitignore +2 -0
  5. data/Gemfile.lock +48 -52
  6. data/bin/console +3 -0
  7. data/bin/dev +4 -0
  8. data/docker-compose.yml +26 -0
  9. data/examples/01-basic-redis-pod/manifest.rb +60 -0
  10. data/examples/database/manifest.rb +238 -0
  11. data/examples/version2/demo.rb +87 -0
  12. data/examples/version2/helpers.rb +18 -0
  13. data/examples/version2/my_app.rb +45 -0
  14. data/examples/version2/postgresql.rb +81 -0
  15. data/examples/version2/ruby_on_rails.rb +31 -0
  16. data/examples/web-app/manifest.rb +215 -0
  17. data/flake.lock +3 -3
  18. data/flake.nix +6 -0
  19. data/kube_cluster.gemspec +3 -1
  20. data/lib/kube/cli/cluster.rb +41 -0
  21. data/lib/kube/cluster/connection.rb +18 -0
  22. data/lib/kube/cluster/instance.rb +21 -0
  23. data/lib/kube/cluster/manifest/middleware/annotations.rb +32 -0
  24. data/lib/kube/cluster/manifest/middleware/hpa_for_deployment.rb +109 -0
  25. data/lib/kube/cluster/manifest/middleware/ingress_for_service.rb +89 -0
  26. data/lib/kube/cluster/manifest/middleware/labels.rb +59 -0
  27. data/lib/kube/cluster/manifest/middleware/namespace.rb +31 -0
  28. data/lib/kube/cluster/manifest/middleware/pod_anti_affinity.rb +61 -0
  29. data/lib/kube/cluster/manifest/middleware/resource_preset.rb +64 -0
  30. data/lib/kube/cluster/manifest/middleware/security_context.rb +84 -0
  31. data/lib/kube/cluster/manifest/middleware/service_for_deployment.rb +69 -0
  32. data/lib/kube/cluster/manifest/middleware.rb +178 -0
  33. data/lib/kube/cluster/manifest/stack.rb +56 -0
  34. data/lib/kube/cluster/manifest.rb +76 -0
  35. data/lib/kube/cluster/resource/dirty_tracking.rb +113 -0
  36. data/lib/kube/cluster/resource/persistence.rb +67 -0
  37. data/lib/kube/cluster/resource.rb +21 -0
  38. data/lib/kube/cluster/version.rb +1 -1
  39. data/lib/kube/cluster.rb +13 -7
  40. data/lib/kube/errors.rb +57 -0
  41. metadata +63 -17
  42. data/Rakefile +0 -11
  43. data/TREE_PLAN.md +0 -513
  44. data/bin/generate-command-schema-v1 +0 -44
  45. data/data/kubectl-command-tree-v1-minimal.json +0 -125
  46. data/data/kubectl-command-tree-v1.json +0 -1469
  47. data/examples/quick-repl/docker-compose.yml +0 -52
  48. data/exe/kube_cluster +0 -6
  49. data/lib/kube/cluster/command_node.rb +0 -89
  50. data/lib/kube/cluster/ctl.rb +0 -33
  51. data/lib/kube/cluster/query_builder.rb +0 -35
  52. data/lib/kube/cluster/resource_selector.rb +0 -19
  53. data/lib/kube/cluster/tree_node.rb +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44abda9fe549953b96a2cb47130457c4c48632f7b15de95b908e5f0f086cd50d
4
- data.tar.gz: b8684b9dc96998ae588ae180709fb2921e9123744dd307c0115566b4a0e3ce0d
3
+ metadata.gz: f6b2fcc6e1de8c59841b080e8150aa249d84fc064f4eab2e981be9ded9dc2290
4
+ data.tar.gz: 1a55810edfb5809b44699e2a17984b9f0311257aba52796da6b74e293ce3a9d0
5
5
  SHA512:
6
- metadata.gz: 8a864cbc779ac254833255e5b747c4e892d38c99328726f6e5f2f10829329918e6952d8370c440fd8821bf3875a4c2d4b3fc43174d387549375bbbfc4bb99794
7
- data.tar.gz: a5435a7443624ba54d4fec5ae93d29cf426a7e5e9c14b160b0d1c925f960d8cb832f50c07962c05039ffcfcdc4a2e581f3bb8b49c4b393307f3dfba286cac989
6
+ metadata.gz: b64081cec15ce53b3a80effecba8f2ae3ec43ee58d1dc483196a821f37a4d6aa0aabbdd3d21e5df7eb5208cbd8bdbcf1aefbf82a9911733795b5ad4689c2c62e
7
+ data.tar.gz: ef610d9b33d6907a92f69c077080a8f26ffd6a1e2f2e391b9f54564187b6799af2e275b2f9f6cb3b056f3f9b3952b964921692a2d4456ee5ee7af229b209ac0c
@@ -0,0 +1,43 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+
10
+
11
+
12
+ build-and-push-gem:
13
+ uses: ./.github/workflows/build-and-push-gem.yml
14
+ permissions:
15
+ contents: read
16
+ id-token: write
17
+ secrets: inherit
18
+
19
+
20
+
21
+ github-release:
22
+
23
+ needs: [build-and-push-gem]
24
+
25
+ runs-on: ubuntu-24.04
26
+ permissions:
27
+ contents: write
28
+ steps:
29
+ - uses: actions/download-artifact@v4
30
+ with:
31
+ path: release/
32
+ merge-multiple: true
33
+
34
+ - name: Consolidate checksums
35
+ run: |
36
+ cd release
37
+ cat SHA256SUMS-linux-*.txt > SHA256SUMS.txt
38
+ rm SHA256SUMS-linux-*.txt
39
+
40
+ - uses: softprops/action-gh-release@v2
41
+ with:
42
+ files: release/*
43
+ generate_release_notes: true
@@ -0,0 +1,47 @@
1
+ name: Auto-tag on version bump
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ tag:
9
+ runs-on: ubuntu-24.04
10
+ permissions:
11
+ contents: write
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: '3.4'
20
+
21
+ - name: Read version from gemspec
22
+ id: read
23
+ run: |
24
+ version=$(ruby -e '
25
+ spec = Gem::Specification.load(Dir["*.gemspec"].first)
26
+ puts spec.version
27
+ ')
28
+ echo "version=$version" >> "$GITHUB_OUTPUT"
29
+
30
+ - name: Check if tag exists
31
+ id: check
32
+ run: |
33
+ tag="v${{ steps.read.outputs.version }}"
34
+ if git rev-parse "$tag" >/dev/null 2>&1; then
35
+ echo "exists=true" >> "$GITHUB_OUTPUT"
36
+ else
37
+ echo "exists=false" >> "$GITHUB_OUTPUT"
38
+ fi
39
+
40
+ - name: Create and push tag
41
+ if: steps.check.outputs.exists == 'false'
42
+ run: |
43
+ tag="v${{ steps.read.outputs.version }}"
44
+ git config user.name "Nathan K"
45
+ git config user.email "nathankidd@hey.com"
46
+ git tag -a "$tag" -m "Release $tag"
47
+ git push origin "$tag"
data/.gitignore CHANGED
@@ -1,6 +1,8 @@
1
1
  *.gem
2
+ .direnv
2
3
  /.bundle/
3
4
  /coverage/
4
5
  /pkg/
5
6
  /tmp/
6
7
  /vendor/bundle
8
+ /kubeconfig.yaml
data/Gemfile.lock CHANGED
@@ -1,52 +1,75 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_cluster (0.1.1)
5
- kube_schema (~> 1.0)
4
+ kube_cluster (0.2.0)
5
+ kube_kit (> 0)
6
+ kube_kubectl (~> 2.0.0)
7
+ kube_schema (~> 1.2.0)
6
8
 
7
9
  GEM
8
10
  remote: https://rubygems.org/
9
11
  specs:
10
- addressable (2.8.9)
11
- public_suffix (>= 2.0.2, < 8.0)
12
12
  ast (2.4.3)
13
- bigdecimal (4.0.1)
13
+ bigdecimal (4.1.2)
14
14
  black_hole_struct (0.1.3)
15
+ date (3.5.1)
16
+ debug (1.11.1)
17
+ irb (~> 1.10)
18
+ reline (>= 0.3.8)
19
+ erb (6.0.3)
15
20
  hana (1.3.7)
16
- json (2.19.2)
17
- json-schema (6.2.0)
18
- addressable (~> 2.8)
19
- bigdecimal (>= 3.1, < 5)
21
+ io-console (0.8.2)
22
+ irb (1.17.0)
23
+ pp (>= 0.6.0)
24
+ prism (>= 1.3.0)
25
+ rdoc (>= 4.0.0)
26
+ reline (>= 0.4.2)
27
+ json (2.19.3)
20
28
  json_schemer (2.5.0)
21
29
  bigdecimal
22
30
  hana (~> 1.3)
23
31
  regexp_parser (~> 2.0)
24
32
  simpleidn (~> 0.2)
25
- kube_schema (1.0.0)
33
+ kube_kit (0.2.0)
34
+ kube_kubectl (2.0.1)
35
+ debug (~> 1.11)
36
+ json_schemer (~> 2.5)
37
+ rubyshell (~> 1.5)
38
+ shellwords (~> 0.2.2)
39
+ string_builder (~> 1.2.0)
40
+ kube_schema (1.2.1)
26
41
  black_hole_struct (~> 0.1)
27
42
  json_schemer (~> 2.5)
28
43
  rubyshell (~> 1.5)
29
44
  language_server-protocol (3.17.0.5)
30
45
  lint_roller (1.1.0)
31
- mcp (0.8.0)
32
- json-schema (>= 4.1)
33
46
  minitest (5.27.0)
34
- parallel (1.27.0)
35
- parser (3.3.10.2)
47
+ parallel (2.0.1)
48
+ parser (3.3.11.1)
36
49
  ast (~> 2.4.1)
37
50
  racc
51
+ pp (0.6.3)
52
+ prettyprint
53
+ prettyprint (0.2.0)
38
54
  prism (1.9.0)
39
- public_suffix (7.0.5)
55
+ psych (5.3.1)
56
+ date
57
+ stringio
40
58
  racc (1.8.1)
41
59
  rainbow (3.1.1)
42
- rake (13.3.1)
43
- regexp_parser (2.11.3)
44
- rubocop (1.85.1)
60
+ rake (13.4.2)
61
+ rdoc (7.2.0)
62
+ erb
63
+ psych (>= 4.0.0)
64
+ tsort
65
+ regexp_parser (2.12.0)
66
+ reline (0.6.3)
67
+ io-console (~> 0.5)
68
+ rubocop (1.86.1)
45
69
  json (~> 2.3)
46
70
  language_server-protocol (~> 3.17.0.2)
47
71
  lint_roller (~> 1.1.0)
48
- mcp (~> 0.6)
49
- parallel (~> 1.10)
72
+ parallel (>= 1.10)
50
73
  parser (>= 3.3.0.2)
51
74
  rainbow (>= 2.2.2, < 4.0)
52
75
  regexp_parser (>= 2.9.3, < 3.0)
@@ -58,7 +81,11 @@ GEM
58
81
  prism (~> 1.7)
59
82
  ruby-progressbar (1.13.0)
60
83
  rubyshell (1.5.0)
84
+ shellwords (0.2.2)
61
85
  simpleidn (0.2.3)
86
+ string_builder (1.2.0)
87
+ stringio (3.2.0)
88
+ tsort (0.2.0)
62
89
  unicode-display_width (3.2.0)
63
90
  unicode-emoji (~> 4.1)
64
91
  unicode-emoji (4.2.0)
@@ -73,36 +100,5 @@ DEPENDENCIES
73
100
  rake (~> 13.0)
74
101
  rubocop (~> 1.21)
75
102
 
76
- CHECKSUMS
77
- addressable (2.8.9) sha256=cc154fcbe689711808a43601dee7b980238ce54368d23e127421753e46895485
78
- ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
79
- bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
80
- black_hole_struct (0.1.3) sha256=b1cac7dbe7f36bb3ed8372de656dbe140ad20d786aaace552c5706f7aa46c4a3
81
- hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
82
- json (2.19.2) sha256=e7e1bd318b2c37c4ceee2444841c86539bc462e81f40d134cf97826cb14e83cf
83
- json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666
84
- json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
85
- kube_cluster (0.1.1)
86
- kube_schema (1.0.0) sha256=a83e584b316f21492fe551231f22cf3e7439fd6f1df6f3769c24d66ab040dc6e
87
- language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
88
- lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
89
- mcp (0.8.0) sha256=ae8bd146bb8e168852866fd26f805f52744f6326afb3211e073f78a95e0c34fb
90
- minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
91
- parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
92
- parser (3.3.10.2) sha256=6f60c84aa4bdcedb6d1a2434b738fe8a8136807b6adc8f7f53b97da9bc4e9357
93
- prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
94
- public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
95
- racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
96
- rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
97
- rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
98
- regexp_parser (2.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
99
- rubocop (1.85.1) sha256=3dbcf9e961baa4c376eeeb2a03913dca5e3987033b04d38fa538aa1e7406cc77
100
- rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
101
- ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
102
- rubyshell (1.5.0) sha256=ffd528415962e52b2f3ec155fc3bc2cac401981413c0db451ea2a20194916ab6
103
- simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
104
- unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
105
- unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
106
-
107
103
  BUNDLED WITH
108
- 4.0.7
104
+ 2.6.9
data/bin/console CHANGED
@@ -5,4 +5,7 @@ require "bundler/setup"
5
5
  require "kube/cluster"
6
6
  require "irb"
7
7
 
8
+ GEM_HOME = File.expand_path('..', __dir__)
9
+ ENV["KUBECONFIG"] = File.join(GEM_HOME, "kubeconfig.yaml")
10
+
8
11
  IRB.start(__FILE__)
data/bin/dev ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+
3
+ docker compose up -d
4
+
@@ -0,0 +1,26 @@
1
+ # to run define K3S_TOKEN, K3S_VERSION is optional, eg:
2
+ # K3S_TOKEN=${RANDOM}${RANDOM}${RANDOM} docker-compose up
3
+ services:
4
+ server:
5
+ image: "rancher/k3s:latest"
6
+ command: server
7
+ tmpfs:
8
+ - /run
9
+ - /var/run
10
+ ulimits:
11
+ nproc: 65535
12
+ nofile:
13
+ soft: 65535
14
+ hard: 65535
15
+ privileged: true
16
+ restart: always
17
+ environment:
18
+ - K3S_TOKEN=change-me-or-face-the-consequences
19
+ - K3S_KUBECONFIG_OUTPUT=/output/kubeconfig.yaml
20
+ - K3S_KUBECONFIG_MODE=666
21
+ volumes:
22
+ - .:/output
23
+ ports:
24
+ - 6443:6443 # Kubernetes API Server
25
+ - 80:80 # Ingress controller port 80
26
+ - 443:443 # Ingress controller port 443
@@ -0,0 +1,60 @@
1
+ require "bundler/setup"
2
+ require "kube/schema"
3
+
4
+ class RedisPod < Kube::Schema['Pod']
5
+ def initialize(container_name: 'my-redis-container', **options, &block)
6
+ super {
7
+ spec.containers = [
8
+ {
9
+ name: container_name,
10
+ image: 'redis:8.0.2',
11
+
12
+ command: ["redis-server", "/redis-master/redis.conf"],
13
+ env: [{name: 'MASTER', value: "true"}],
14
+ ports: [{ containerPort: 6379 }],
15
+
16
+ resources: { limits: { cpu: "0.1" } },
17
+ volumeMounts: [
18
+ { mountPath: '/redis-master-data', name: 'data' },
19
+ { mountPath: '/redis-master', name: 'config' },
20
+ ]
21
+ }
22
+ ]
23
+
24
+ spec.volumes = [
25
+ {
26
+ name: 'data',
27
+ emptyDir: {}
28
+ },
29
+ {
30
+ name: 'config',
31
+ configMap: {
32
+ name: 'example-redis-config',
33
+ items: [ { key: 'redis-config', path: 'redis.conf' } ]
34
+ }
35
+ },
36
+ ]
37
+ }
38
+ instance_exec(&block) if block_given?
39
+ end
40
+ end
41
+
42
+ puts RedisPod.new(
43
+ container_name: 'my-redis-container-1',
44
+ metadata: {
45
+ namespace: 'my-namespace'
46
+ }
47
+ ).to_yaml
48
+
49
+ puts RedisPod.new(container_name: 'my-redis-container-1') {
50
+ metadata.namespace = 'my-namespace'
51
+ }.to_yaml
52
+
53
+ puts RedisPod.new {
54
+ metadata.namespace = "my-namespace"
55
+ }.to_yaml
56
+
57
+ puts RedisPod.new {
58
+ metadata.name = "my-redis-1"
59
+ metadata.namespace = "my-namespace"
60
+ }.to_yaml
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Database (PostgreSQL) Example
5
+ #
6
+ # Demonstrates the Ruby equivalents of Bitnami common chart patterns:
7
+ # - Secret lifecycle management (_secrets.tpl)
8
+ # - StorageClass resolution (_storage.tpl)
9
+ # - StatefulSet with persistent volumes
10
+ # - Headless service for stable DNS
11
+ # - NetworkPolicy for database isolation
12
+ # - Standard labels and naming (_labels.tpl, _names.tpl)
13
+ # - Resource presets (_resources.tpl)
14
+ #
15
+ # Usage:
16
+ # ruby examples/database/manifest.rb
17
+ # ruby examples/database/manifest.rb > database.yaml
18
+
19
+ require "kube/schema"
20
+ require "securerandom"
21
+
22
+ # ── Naming ────────────────────────────────────────────────────────────────────
23
+
24
+ APP_NAME = "postgresql"
25
+ RELEASE_NAME = "my-release"
26
+ NAMESPACE = "database"
27
+ FULLNAME = "#{RELEASE_NAME}-#{APP_NAME}"[0, 63].chomp("-")
28
+
29
+ # ── Labels ────────────────────────────────────────────────────────────────────
30
+
31
+ STANDARD_LABELS = {
32
+ "app.kubernetes.io/name": APP_NAME,
33
+ "app.kubernetes.io/instance": RELEASE_NAME,
34
+ "app.kubernetes.io/version": "16.4.0",
35
+ "app.kubernetes.io/component": "primary",
36
+ "app.kubernetes.io/managed-by": "kube_cluster",
37
+ }
38
+
39
+ MATCH_LABELS = STANDARD_LABELS.slice(
40
+ :"app.kubernetes.io/name",
41
+ :"app.kubernetes.io/instance",
42
+ :"app.kubernetes.io/component",
43
+ )
44
+
45
+ # ── Secrets (from _secrets.tpl) ───────────────────────────────────────────────
46
+ # Bitnami's secrets.passwords.manage generates random passwords, reuses existing
47
+ # ones on upgrade, and base64 encodes them. In Ruby we do this directly.
48
+
49
+ POSTGRES_PASSWORD = SecureRandom.alphanumeric(24)
50
+ REPLICATION_PASSWORD = SecureRandom.alphanumeric(24)
51
+
52
+ def base64(str)
53
+ [str].pack("m0")
54
+ end
55
+
56
+ # ── Storage (from _storage.tpl) ───────────────────────────────────────────────
57
+ # Bitnami resolves storage class from global > persistence > default.
58
+ # The "-" convention means explicitly use the default storage class (empty string).
59
+
60
+ STORAGE_CLASS = "standard" # set to "-" for default, or nil to omit
61
+ STORAGE_SIZE = "10Gi"
62
+
63
+ # ── Resource presets ──────────────────────────────────────────────────────────
64
+
65
+ RESOURCES = {
66
+ requests: { cpu: "500m", memory: "512Mi" },
67
+ limits: { cpu: "750m", memory: "768Mi" },
68
+ }
69
+
70
+ # ── Build manifests ───────────────────────────────────────────────────────────
71
+
72
+ manifest = Kube::Schema::Manifest.new
73
+
74
+ # -- Namespace --
75
+
76
+ manifest << Kube::Schema["Namespace"].new {
77
+ metadata.name = NAMESPACE
78
+ metadata.labels = STANDARD_LABELS.reject { |k, _| k == :"app.kubernetes.io/component" }
79
+ }
80
+
81
+ # -- Secret --
82
+ # Pattern from _secrets.tpl: base64-encoded credentials, separate keys for
83
+ # each password, supports existing secret reuse on upgrade.
84
+
85
+ manifest << Kube::Schema["Secret"].new {
86
+ metadata.name = FULLNAME
87
+ metadata.namespace = NAMESPACE
88
+ metadata.labels = STANDARD_LABELS
89
+ self.type = "Opaque"
90
+ self.data = {
91
+ "postgres-password": base64(POSTGRES_PASSWORD),
92
+ "replication-password": base64(REPLICATION_PASSWORD),
93
+ }
94
+ }
95
+
96
+ # -- Headless Service (for StatefulSet stable DNS) --
97
+
98
+ manifest << Kube::Schema["Service"].new {
99
+ metadata.name = "#{FULLNAME}-headless"
100
+ metadata.namespace = NAMESPACE
101
+ metadata.labels = STANDARD_LABELS
102
+
103
+ spec.clusterIP = "None"
104
+ spec.selector = MATCH_LABELS
105
+ spec.ports = [
106
+ { name: "tcp-postgresql", port: 5432, targetPort: "tcp-postgresql" },
107
+ ]
108
+ }
109
+
110
+ # -- Primary Service (for client connections) --
111
+
112
+ manifest << Kube::Schema["Service"].new {
113
+ metadata.name = FULLNAME
114
+ metadata.namespace = NAMESPACE
115
+ metadata.labels = STANDARD_LABELS
116
+
117
+ spec.selector = MATCH_LABELS
118
+ spec.ports = [
119
+ { name: "tcp-postgresql", port: 5432, targetPort: "tcp-postgresql" },
120
+ ]
121
+ }
122
+
123
+ # -- StatefulSet --
124
+ # Uses storage class resolution pattern, secret references, resource presets,
125
+ # pod anti-affinity for spreading replicas.
126
+
127
+ manifest << Kube::Schema["StatefulSet"].new {
128
+ metadata.name = FULLNAME
129
+ metadata.namespace = NAMESPACE
130
+ metadata.labels = STANDARD_LABELS
131
+
132
+ spec.serviceName = "#{FULLNAME}-headless"
133
+ spec.replicas = 1
134
+ spec.selector.matchLabels = MATCH_LABELS
135
+
136
+ spec.template.metadata.labels = STANDARD_LABELS
137
+ spec.template.spec.containers = [
138
+ {
139
+ name: APP_NAME,
140
+ image: "docker.io/postgres:16.4-alpine",
141
+ ports: [
142
+ { name: "tcp-postgresql", containerPort: 5432 },
143
+ ],
144
+ resources: RESOURCES,
145
+ env: [
146
+ { name: "POSTGRES_PASSWORD", valueFrom: { secretKeyRef: { name: FULLNAME, key: "postgres-password" } } },
147
+ { name: "PGDATA", value: "/var/lib/postgresql/data/pgdata" },
148
+ ],
149
+ volumeMounts: [
150
+ { name: "data", mountPath: "/var/lib/postgresql/data" },
151
+ ],
152
+ livenessProbe: {
153
+ exec: { command: ["pg_isready", "-U", "postgres"] },
154
+ initialDelaySeconds: 30,
155
+ periodSeconds: 10,
156
+ timeoutSeconds: 5,
157
+ failureThreshold: 6,
158
+ },
159
+ readinessProbe: {
160
+ exec: { command: ["pg_isready", "-U", "postgres"] },
161
+ initialDelaySeconds: 5,
162
+ periodSeconds: 10,
163
+ timeoutSeconds: 5,
164
+ failureThreshold: 6,
165
+ },
166
+ },
167
+ ]
168
+
169
+ # Pod anti-affinity: hard anti-affinity to guarantee one pod per node
170
+ # (from _affinities.tpl: common.affinities.pods.hard)
171
+ spec.template.spec.affinity = {
172
+ podAntiAffinity: {
173
+ requiredDuringSchedulingIgnoredDuringExecution: [
174
+ {
175
+ labelSelector: { matchLabels: MATCH_LABELS },
176
+ topologyKey: "kubernetes.io/hostname",
177
+ },
178
+ ],
179
+ },
180
+ }
181
+
182
+ # Storage class resolution (from _storage.tpl)
183
+ storage_class = STORAGE_CLASS == "-" ? "" : STORAGE_CLASS
184
+
185
+ spec.volumeClaimTemplates = [
186
+ {
187
+ metadata: { name: "data" },
188
+ spec: {
189
+ accessModes: ["ReadWriteOnce"],
190
+ storageClassName: storage_class,
191
+ resources: { requests: { storage: STORAGE_SIZE } },
192
+ },
193
+ },
194
+ ]
195
+ }
196
+
197
+ # -- NetworkPolicy --
198
+ # Isolate the database: only allow ingress from pods with the app label,
199
+ # deny everything else. This is a common production hardening pattern.
200
+
201
+ manifest << Kube::Schema["NetworkPolicy"].new {
202
+ metadata.name = FULLNAME
203
+ metadata.namespace = NAMESPACE
204
+ metadata.labels = STANDARD_LABELS
205
+
206
+ spec.podSelector = { matchLabels: MATCH_LABELS }
207
+ spec.policyTypes = ["Ingress", "Egress"]
208
+ spec.ingress = [
209
+ {
210
+ from: [
211
+ {
212
+ podSelector: {
213
+ matchLabels: { "app.kubernetes.io/name": "web-app" },
214
+ },
215
+ },
216
+ ],
217
+ ports: [
218
+ { protocol: "TCP", port: "5432" },
219
+ ],
220
+ },
221
+ ]
222
+ # Allow DNS egress + nothing else
223
+ spec.egress = [
224
+ {
225
+ to: [
226
+ { namespaceSelector: {} },
227
+ ],
228
+ ports: [
229
+ { protocol: "UDP", port: "53" },
230
+ { protocol: "TCP", port: "53" },
231
+ ],
232
+ },
233
+ ]
234
+ }
235
+
236
+ # ── Render ────────────────────────────────────────────────────────────────────
237
+
238
+ puts manifest.to_yaml
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Middleware-driven Manifest Example
5
+ #
6
+ # Demonstrates how middleware eliminates boilerplate. The middleware stack
7
+ # declared in MyApp automatically generates Services, Ingresses, HPAs,
8
+ # and injects resource limits, security contexts, and pod anti-affinity.
9
+ #
10
+ # The block below only declares the unique intent — the things only a
11
+ # human knows. Everything else is derived by middleware from labels.
12
+ #
13
+ # What middleware generates from each Deployment:
14
+ # - Service (from container ports + matchLabels)
15
+ # - Ingress (from app.kubernetes.io/expose label)
16
+ # - HPA (from app.kubernetes.io/autoscale label)
17
+ # - Resource limits (from app.kubernetes.io/size label)
18
+ # - Security contexts (restricted profile, all pod-bearing resources)
19
+ # - Pod anti-affinity (spread across nodes, all pod-bearing resources)
20
+ # - Standard labels (managed-by, merged into everything)
21
+ #
22
+ # Usage:
23
+ # ruby examples/version2/demo.rb
24
+ # ruby examples/version2/demo.rb > app.yaml
25
+
26
+ require "kube/cluster"
27
+ require "securerandom"
28
+ require_relative "my_app"
29
+
30
+ app = MyApp.new("example.com", size: :small) do |m|
31
+ name = "rails-app"
32
+ ns = "production"
33
+ db_name = "postgresql"
34
+ db_ns = "database"
35
+
36
+ labels = m.app_labels(name: name, instance: name)
37
+ db_labels = m.app_labels(name: db_name, instance: db_name, component: "primary")
38
+ db_match = m.match_labels(name: db_name, instance: db_name, component: "primary")
39
+
40
+ # ── Rails tier ──────────────────────────────────────────────────
41
+ #
42
+ # One Namespace, one ConfigMap, one Deployment.
43
+ # Middleware generates: Service, Ingress, HPA
44
+ # Middleware injects: resource limits, security context, anti-affinity, labels
45
+
46
+ [
47
+ Kube::Schema["Namespace"].new {
48
+ metadata.name = ns
49
+ metadata.labels = labels
50
+ },
51
+
52
+ Kube::Schema["ConfigMap"].new {
53
+ metadata.name = "#{name}-config"
54
+ metadata.namespace = ns
55
+ metadata.labels = labels
56
+ self.data = {
57
+ RAILS_ENV: "production",
58
+ DATABASE_URL: "postgres://#{db_name}-headless.#{db_ns}.svc.cluster.local:5432/app",
59
+ LOG_LEVEL: "info",
60
+ WORKERS: "4",
61
+ }
62
+ },
63
+
64
+ RubyOnRails.new {
65
+ metadata.name = name
66
+ metadata.namespace = ns
67
+ metadata.labels = labels.merge(
68
+ "app.kubernetes.io/expose": "app.example.com",
69
+ "app.kubernetes.io/autoscale": "1-5",
70
+ )
71
+ },
72
+ ]
73
+
74
+ # ── Database tier ───────────────────────────────────────────────
75
+ #
76
+ # StatefulSet + headless Service + Secret + NetworkPolicy.
77
+ # Middleware generates: Service (from container ports)
78
+ # Middleware injects: resource limits, security context, anti-affinity, labels
79
+
80
+ pg_password = SecureRandom.alphanumeric(24)
81
+
82
+ Postgresql.new {
83
+ }
84
+
85
+ end
86
+
87
+ puts app.to_yaml