kube_cluster 0.2.0 → 0.3.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 (59) 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/02-manifest-with-middleware/manifest.rb +37 -0
  11. data/examples/02-manifest-with-middleware/middleware/labels.rb +4 -0
  12. data/examples/02-manifest-with-middleware/middleware/namespace.rb +4 -0
  13. data/examples/02-manifest-with-middleware/templates/config_map.rb +13 -0
  14. data/examples/02-manifest-with-middleware/templates/deployment.rb +59 -0
  15. data/examples/02-manifest-with-middleware/templates/horizontal_pod_autoscaler.rb +30 -0
  16. data/examples/02-manifest-with-middleware/templates/ingress.rb +38 -0
  17. data/examples/02-manifest-with-middleware/templates/service.rb +12 -0
  18. data/examples/03-app-with-database/demo.rb +87 -0
  19. data/examples/03-app-with-database/helpers.rb +18 -0
  20. data/examples/03-app-with-database/my_app.rb +45 -0
  21. data/examples/03-app-with-database/postgresql.rb +81 -0
  22. data/examples/03-app-with-database/ruby_on_rails.rb +31 -0
  23. data/flake.lock +3 -3
  24. data/flake.nix +6 -0
  25. data/kube_cluster.gemspec +3 -1
  26. data/lib/kube/cli/cluster.rb +41 -0
  27. data/lib/kube/cluster/connection.rb +18 -0
  28. data/lib/kube/cluster/instance.rb +21 -0
  29. data/lib/kube/cluster/manifest.rb +25 -0
  30. data/lib/kube/cluster/middleware/annotations.rb +32 -0
  31. data/lib/kube/cluster/middleware/hpa_for_deployment.rb +111 -0
  32. data/lib/kube/cluster/middleware/ingress_for_service.rb +91 -0
  33. data/lib/kube/cluster/middleware/labels.rb +59 -0
  34. data/lib/kube/cluster/middleware/namespace.rb +31 -0
  35. data/lib/kube/cluster/middleware/pod_anti_affinity.rb +61 -0
  36. data/lib/kube/cluster/middleware/resource_preset.rb +64 -0
  37. data/lib/kube/cluster/middleware/security_context.rb +84 -0
  38. data/lib/kube/cluster/middleware/service_for_deployment.rb +71 -0
  39. data/lib/kube/cluster/middleware/stack.rb +43 -0
  40. data/lib/kube/cluster/middleware.rb +69 -0
  41. data/lib/kube/cluster/resource/dirty_tracking.rb +113 -0
  42. data/lib/kube/cluster/resource/persistence.rb +67 -0
  43. data/lib/kube/cluster/resource.rb +99 -0
  44. data/lib/kube/cluster/version.rb +1 -1
  45. data/lib/kube/cluster.rb +34 -7
  46. data/lib/kube/errors.rb +57 -0
  47. metadata +69 -17
  48. data/Rakefile +0 -11
  49. data/TREE_PLAN.md +0 -513
  50. data/bin/generate-command-schema-v1 +0 -44
  51. data/data/kubectl-command-tree-v1-minimal.json +0 -125
  52. data/data/kubectl-command-tree-v1.json +0 -1469
  53. data/examples/quick-repl/docker-compose.yml +0 -52
  54. data/exe/kube_cluster +0 -6
  55. data/lib/kube/cluster/command_node.rb +0 -89
  56. data/lib/kube/cluster/ctl.rb +0 -33
  57. data/lib/kube/cluster/query_builder.rb +0 -35
  58. data/lib/kube/cluster/resource_selector.rb +0 -19
  59. 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: f35a6fb5b6ece5ffc32651a5ad104c896bc8cb80810c80ecb560a22fe0a59eca
4
+ data.tar.gz: df13e8af081ff1bba21254570f12eef80f82fe2ab47804877e76c0f360c9212c
5
5
  SHA512:
6
- metadata.gz: 8a864cbc779ac254833255e5b747c4e892d38c99328726f6e5f2f10829329918e6952d8370c440fd8821bf3875a4c2d4b3fc43174d387549375bbbfc4bb99794
7
- data.tar.gz: a5435a7443624ba54d4fec5ae93d29cf426a7e5e9c14b160b0d1c925f960d8cb832f50c07962c05039ffcfcdc4a2e581f3bb8b49c4b393307f3dfba286cac989
6
+ metadata.gz: cbfd8728fd22e79cc2429dc503cbba4f931da5d2f36e99d210b189863bdaa9fcf21965b13b1af231029d973dfafc03f1d44dba48f0f51eb07ba3d6597d8dfea1
7
+ data.tar.gz: 4dd4cec8ee94d15cce4430e6ee1b6a80a5db1e5aa97995b4aa58c3d5180dc9ac7829dbb4d36e9ba2b55c17ec025de097d0622d808dc24ba9e87ee5fadf4a2478
@@ -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.1)
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.4)
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.2)
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.3)
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::Cluster['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,37 @@
1
+ require_relative 'templates/config_map'
2
+ require_relative 'templates/deployment'
3
+ require_relative 'templates/ingress'
4
+ require_relative 'templates/service'
5
+ require_relative 'templates/horizontal_pod_autoscaler'
6
+
7
+ require_relative 'middlware/labels'
8
+ require_relative 'middlware/namespace'
9
+
10
+ class MyApp < Kube::Schema::Manifest
11
+ stack do
12
+ use Middleware::Namespace
13
+ use Middleware::Labels
14
+ end
15
+ end
16
+
17
+ puts MyApp.new(
18
+ Templates::ConfigMap.new {
19
+ # no overrides today
20
+ },
21
+
22
+ Templates::Deployment.new {
23
+ # no overrides today
24
+ },
25
+
26
+ Templates::Ingress.new {
27
+ # no overrides today
28
+ },
29
+
30
+ Templates::Service.new {
31
+ # no overrides today
32
+ },
33
+
34
+ Templates::HorizontalPodScaler.new {
35
+ # no overrides today
36
+ },
37
+ ).to_yaml
@@ -0,0 +1,4 @@
1
+ module Middleware
2
+ class Labels < Kube::Cluster::Manifest::Middleware
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Middleware
2
+ class Namespace < Kube::Cluster::Manifest::Middleware
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ class ConfigMap < Kube::Cluster["ConfigMap"]
2
+ def initialize(namespace:)
3
+ build {
4
+ metadata.name = "#{namespace}-config"
5
+ spec.data = {
6
+ RAILS_ENV: "production",
7
+ LOG_LEVEL: "info",
8
+ WORKERS: "4",
9
+ PORT: "3000",
10
+ }
11
+ }
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ class Deployment < Kube::Cluster["Deployment"]
2
+ def initialize(namespace:)
3
+ build {
4
+ metadata.name = namespace
5
+
6
+ spec.replicas = 3
7
+ spec.selector.matchLabels = MATCH_LABELS
8
+
9
+ spec.template.metadata.labels = STANDARD_LABELS
10
+ spec.template.metadata.annotations = {
11
+ # Checksum pattern from _utils.tpl -- triggers rolling restart on config change
12
+ "checksum/config": "{{ sha256sum of configmap data }}",
13
+ }
14
+
15
+ spec.template.spec.containers = [
16
+ {
17
+ name: APP_NAME,
18
+ image: IMAGE,
19
+ ports: [{ name: "http", containerPort: 3000, protocol: "TCP" }],
20
+ resources: RESOURCES,
21
+ env: [
22
+ { name: "PORT", value: "3000" },
23
+ ],
24
+ envFrom: [
25
+ { configMapRef: { name: "#{FULLNAME}-config" } },
26
+ ],
27
+ livenessProbe: {
28
+ httpGet: { path: "/healthz", port: "http" },
29
+ initialDelaySeconds: 15,
30
+ periodSeconds: 10,
31
+ },
32
+ readinessProbe: {
33
+ httpGet: { path: "/readyz", port: "http" },
34
+ initialDelaySeconds: 5,
35
+ periodSeconds: 5,
36
+ },
37
+ },
38
+ ]
39
+
40
+ # Pod anti-affinity (from _affinities.tpl)
41
+ # Soft anti-affinity: prefer spreading pods across nodes but don't enforce it
42
+ spec.template.spec.affinity = {
43
+ podAntiAffinity: {
44
+ preferredDuringSchedulingIgnoredDuringExecution: [
45
+ {
46
+ weight: 1,
47
+ podAffinityTerm: {
48
+ labelSelector: {
49
+ matchLabels: MATCH_LABELS,
50
+ },
51
+ topologyKey: "kubernetes.io/hostname",
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ }
57
+ }
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ class HorizontalPodAutoscaler < Kube::Cluster["HorizontalPodAutoscaler"]
2
+ def initialize(namespace:)
3
+ build {
4
+ metadata.name = namespace
5
+
6
+ spec.scaleTargetRef.apiVersion = "apps/v1"
7
+ spec.scaleTargetRef.kind = "Deployment"
8
+ spec.scaleTargetRef.name = namespace
9
+
10
+ spec.minReplicas = 3
11
+ spec.maxReplicas = 10
12
+ spec.metrics = [
13
+ {
14
+ type: "Resource",
15
+ resource: {
16
+ name: "cpu",
17
+ target: { type: "Utilization", averageUtilization: 75 },
18
+ },
19
+ },
20
+ {
21
+ type: "Resource",
22
+ resource: {
23
+ name: "memory",
24
+ target: { type: "Utilization", averageUtilization: 80 },
25
+ },
26
+ },
27
+ ]
28
+ }
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ class Ingress < Kube::Cluster["Ingress"]
2
+ def initialize(namespace:)
3
+ build {
4
+ metadata.name = namespace
5
+ metadata.annotations = {
6
+ "cert-manager.io/cluster-issuer": "letsencrypt-prod",
7
+ "nginx.ingress.kubernetes.io/ssl-redirect": "true",
8
+ }
9
+
10
+ spec.ingressClassName = "nginx"
11
+ spec.tls = [
12
+ {
13
+ hosts: ["app.example.com"],
14
+ secretName: "#{namespace}-tls",
15
+ },
16
+ ]
17
+ spec.rules = [
18
+ {
19
+ host: "app.example.com",
20
+ http: {
21
+ paths: [
22
+ {
23
+ path: "/",
24
+ pathType: "Prefix",
25
+ backend: {
26
+ service: {
27
+ name: FULLNAME,
28
+ port: { name: "http" },
29
+ },
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ },
35
+ ]
36
+ }
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ class Service < Kube::Cluster["Service"]
2
+ def initialize(namespace:)
3
+ build {
4
+ metadata.name = namespace
5
+
6
+ spec.selector = MATCH_LABELS
7
+ spec.ports = [
8
+ { name: "http", port: 80, targetPort: "http", protocol: "TCP" },
9
+ ]
10
+ }
11
+ end
12
+ end
@@ -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::Cluster["Namespace"].new {
48
+ metadata.name = ns
49
+ metadata.labels = labels
50
+ },
51
+
52
+ Kube::Cluster["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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module App
4
+ module Helpers
5
+ def match_labels(name:, instance:, component: nil)
6
+ labels = {
7
+ "app.kubernetes.io/name": name,
8
+ "app.kubernetes.io/instance": instance,
9
+ }
10
+ labels[:"app.kubernetes.io/component"] = component if component
11
+ labels
12
+ end
13
+
14
+ def base64(str)
15
+ [str].pack("m0")
16
+ end
17
+ end
18
+ end