kube_cluster 0.3.7 → 0.3.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de5958c26954185923d57f941d9ecea6972b30a7df0a2014e691621c62f731f9
4
- data.tar.gz: f593ad383f34c30cf5a3665f32fceee285fd4eb7dc1302bd7189e36b3680f5ed
3
+ metadata.gz: 6ac91f4fe14ccc5bf1c7e5e1465d2c9568276a0cb3c3623ee5938679deea4d1b
4
+ data.tar.gz: cf89241b112caf7f919c83b1ffba51883005c7600415b6b626449521ac452fda
5
5
  SHA512:
6
- metadata.gz: 3610b3f4b74e0c547746bf155f0b717d0cc5a4f7229b04defd35d3baacf5151ddfdda0ba9ac389764a7dc5335d80042062ce178adb04c4485126e68a0cceda60
7
- data.tar.gz: a1a31a19a6bd3995b924250f55fe91ddd9397cb20d3dd5d1b243bd01abda5cc6e44aa062e9b4b4b4def02a766f6857be654a959861695c508a5612a443a02d06
6
+ metadata.gz: 03b65216d55822e7abfaf52f6cdb1b471cc2797e5f57354de6ddc3f8ee688f7954ab9f627499d31d425ce42073bde2f1623ac65314be8d205aeac647e42f3513
7
+ data.tar.gz: 58400d0fddf0e803578bcc04dec147e3a092da4ec694a45ac98435011558e35782b3b3cac86303b9146514cfb72d6c0b5fb5dbf041cbf0bd2683ea6e6948c006
data/Gemfile.lock CHANGED
@@ -1,15 +1,19 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_cluster (0.3.7)
5
- kube_kubectl (~> 2.0.0)
6
- kube_schema (~> 1.3.0)
4
+ kube_cluster (0.3.9)
5
+ kube_kubectl (~> 2.0.9)
6
+ kube_schema (~> 1.3.7)
7
+ scampi (~> 0.1)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
10
11
  specs:
11
12
  ast (2.4.3)
12
13
  bigdecimal (4.1.2)
14
+ colorize (1.1.0)
15
+ colorize-extended (0.1.0)
16
+ colorize (~> 1.1)
13
17
  date (3.5.1)
14
18
  debug (1.11.1)
15
19
  irb (~> 1.10)
@@ -28,18 +32,17 @@ GEM
28
32
  hana (~> 1.3)
29
33
  regexp_parser (~> 2.0)
30
34
  simpleidn (~> 0.2)
31
- kube_kubectl (2.0.8)
35
+ kube_kubectl (2.0.9)
32
36
  debug (~> 1.11)
33
37
  json_schemer (~> 2.5)
34
38
  rubyshell (~> 1.5)
35
39
  shellwords (~> 0.2.2)
36
- string_builder (~> 1.2.0)
37
- kube_schema (1.3.4)
40
+ string_builder (~> 1.2.2)
41
+ kube_schema (1.3.7)
38
42
  json_schemer (~> 2.5.0)
39
43
  rubyshell (~> 1.5.0)
40
44
  language_server-protocol (3.17.0.5)
41
45
  lint_roller (1.1.0)
42
- minitest (5.27.0)
43
46
  parallel (2.0.1)
44
47
  parser (3.3.11.1)
45
48
  ast (~> 2.4.1)
@@ -77,9 +80,11 @@ GEM
77
80
  prism (~> 1.7)
78
81
  ruby-progressbar (1.13.0)
79
82
  rubyshell (1.5.0)
83
+ scampi (0.1.4)
84
+ colorize-extended
80
85
  shellwords (0.2.2)
81
86
  simpleidn (0.2.3)
82
- string_builder (1.2.1)
87
+ string_builder (1.2.2)
83
88
  stringio (3.2.0)
84
89
  tsort (0.2.0)
85
90
  unicode-display_width (3.2.0)
@@ -92,7 +97,6 @@ PLATFORMS
92
97
 
93
98
  DEPENDENCIES
94
99
  kube_cluster!
95
- minitest (~> 5.0)
96
100
  rake (~> 13.0)
97
101
  rubocop (~> 1.21)
98
102
 
data/README.md CHANGED
@@ -1,15 +1,138 @@
1
1
  # kube_cluster
2
2
 
3
- A template for creating Ruby gems.
3
+ Ruby-native Kubernetes. Define, transform, and deploy cluster resources with pure Ruby.
4
4
 
5
- ## Usage
5
+ ## Install
6
6
 
7
- ```bash
8
- git clone https://github.com/n-at-han-k/kube_cluster
9
- cd kube_cluster
10
- bin/rename-gem my-new-gem
11
- bin/update-spec
12
- bin/choose-license
13
- bin/increment-version patch
14
- bin/release-gem
7
+ ```ruby
8
+ gem "kube_cluster", "~> 0.3"
15
9
  ```
10
+
11
+ ## Examples
12
+
13
+ ### Define a resource
14
+
15
+ ```ruby
16
+ pod = Kube::Cluster["Pod"].new {
17
+ metadata.name = "redis"
18
+ spec.containers = [{ name: "redis", image: "redis:8" }]
19
+ }
20
+
21
+ puts pod.to_yaml
22
+ ```
23
+
24
+ ### Subclass for reuse
25
+
26
+ ```ruby
27
+ class RedisPod < Kube::Cluster["Pod"]
28
+ def initialize(&block)
29
+ super {
30
+ metadata.name = "redis"
31
+ spec.containers = [{ name: "redis", image: "redis:8", ports: [{ containerPort: 6379 }] }]
32
+ }
33
+ instance_exec(&block) if block_given?
34
+ end
35
+ end
36
+
37
+ puts RedisPod.new { metadata.namespace = "production" }.to_yaml
38
+ ```
39
+
40
+ ### Manifest + middleware
41
+
42
+ One Deployment declaration becomes a fully-configured stack:
43
+
44
+ ```ruby
45
+ manifest = Kube::Cluster::Manifest.new(
46
+ Kube::Cluster["Deployment"].new {
47
+ metadata.name = "web"
48
+ metadata.labels = {
49
+ "app.kubernetes.io/expose": "app.example.com",
50
+ "app.kubernetes.io/autoscale": "2-10",
51
+ "app.kubernetes.io/size": "small"
52
+ }
53
+ spec.selector.matchLabels = { app: "web" }
54
+ spec.template.spec.containers = [
55
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] }
56
+ ]
57
+ }
58
+ )
59
+
60
+ Kube::Cluster::Middleware::Stack.new {
61
+ use Middleware::ServiceForDeployment
62
+ use Middleware::IngressForService
63
+ use Middleware::HPAForDeployment
64
+ use Middleware::Namespace, "production"
65
+ use Middleware::Labels, managed_by: "kube_cluster"
66
+ use Middleware::ResourcePreset
67
+ use Middleware::SecurityContext
68
+ use Middleware::PodAntiAffinity
69
+ }.call(manifest)
70
+
71
+ puts manifest.to_yaml # => Deployment, Service, Ingress, HPA — all configured
72
+ ```
73
+
74
+ ### Dirty tracking + patching
75
+
76
+ ```ruby
77
+ cluster = Kube::Cluster.connect(kubeconfig: "~/.kube/config")
78
+
79
+ config = Kube::Cluster["ConfigMap"].new(cluster:) {
80
+ metadata.name = "app-config"
81
+ self.data = { version: "1" }
82
+ }
83
+
84
+ config.apply # creates on cluster
85
+ config.data.version = "2"
86
+ config.changed? # => true
87
+ config.patch # sends only { data: { version: "2" } }
88
+ ```
89
+
90
+ ### Helm charts as manifests
91
+
92
+ ```ruby
93
+ manifest = Kube::Helm::Repo
94
+ .new("bitnami", url: "https://charts.bitnami.com/bitnami")
95
+ .fetch("nginx", version: "18.1.0")
96
+ .apply_values("replicaCount" => 3)
97
+
98
+ puts manifest.to_yaml
99
+ ```
100
+
101
+ ### Register CRDs as first-class resources
102
+
103
+ ```ruby
104
+ chart = Kube::Helm::Repo.new("jetstack", url: "https://charts.jetstack.io")
105
+ .fetch("cert-manager", version: "1.17.2")
106
+
107
+ chart.crds.each { |crd|
108
+ s = crd.to_json_schema
109
+ Kube::Schema.register(s[:kind], schema: s[:schema], api_version: s[:api_version])
110
+ }
111
+
112
+ issuer = Kube::Cluster["ClusterIssuer"].new {
113
+ metadata.name = "letsencrypt"
114
+ spec.acme.server = "https://acme-v02.api.letsencrypt.org/directory"
115
+ }
116
+ ```
117
+
118
+ ## Middleware
119
+
120
+ | Middleware | Effect |
121
+ |---|---|
122
+ | `Namespace` | Sets `metadata.namespace` on all resources |
123
+ | `Labels` | Merges standard Kubernetes labels |
124
+ | `Annotations` | Merges annotations |
125
+ | `ResourcePreset` | Injects CPU/memory from `app.kubernetes.io/size` (nano → 2xlarge) |
126
+ | `SecurityContext` | Injects restricted/baseline security contexts |
127
+ | `PodAntiAffinity` | Spreads pods across nodes |
128
+ | `ServiceForDeployment` | Generates Service from named container ports |
129
+ | `IngressForService` | Generates Ingress from `app.kubernetes.io/expose` label |
130
+ | `HPAForDeployment` | Generates HPA from `app.kubernetes.io/autoscale` label |
131
+
132
+ ## More examples
133
+
134
+ See the [`examples/`](examples/) directory for complete runnable projects.
135
+
136
+ ## License
137
+
138
+ Apache-2.0
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ task :test do
4
+ sh "bundle", "exec", "scampi"
5
+ end
6
+
7
+ task default: :test
data/bin/test CHANGED
@@ -1,14 +1,3 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
1
+ #!/usr/bin/env bash
3
2
 
4
- $LOAD_PATH.unshift File.expand_path("../test", __dir__)
5
-
6
- require "bundler/setup"
7
-
8
- Dir.chdir(File.expand_path("..", __dir__))
9
-
10
- if ARGV.empty?
11
- Dir.glob("test/**/*_test.rb").sort.each { |f| require_relative "../#{f}" }
12
- else
13
- ARGV.each { |f| require_relative "../#{f}" }
14
- end
3
+ bundle exec scampi $@
data/kube_cluster.gemspec CHANGED
@@ -28,10 +28,11 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ["lib"]
30
30
 
31
- spec.add_development_dependency "minitest", "~> 5.0"
31
+ spec.add_dependency "scampi", "~> 0.1"
32
+
32
33
  spec.add_development_dependency "rake", "~> 13.0"
33
34
  spec.add_development_dependency "rubocop", "~> 1.21"
34
35
 
35
- spec.add_dependency "kube_schema", "~> 1.3.0"
36
- spec.add_dependency "kube_kubectl", "~> 2.0.0"
36
+ spec.add_dependency "kube_schema", "~> 1.3.7"
37
+ spec.add_dependency "kube_kubectl", "~> 2.0.9"
37
38
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "kube/cluster"
5
+
3
6
  module Kube
4
7
  module Cluster
5
8
  # A flat, ordered collection of Kubernetes resources.
@@ -23,3 +26,205 @@ module Kube
23
26
  end
24
27
  end
25
28
  end
29
+
30
+ test do
31
+ Middleware = Kube::Cluster::Middleware
32
+
33
+ # ── Bare manifest ────────────────────────────────────────────────────────
34
+
35
+ it "bare_manifest_enumerates_resources_unchanged" do
36
+ m = Kube::Cluster::Manifest.new
37
+ m << Kube::Cluster["ConfigMap"].new {
38
+ metadata.name = "test"
39
+ self.data = { key: "value" }
40
+ }
41
+
42
+ resources = m.to_a
43
+ resources.size.should == 1
44
+ end
45
+
46
+ # ── Stack transforms resources ───────────────────────────────────────────
47
+
48
+ it "stack_transforms_resources" do
49
+ m = Kube::Cluster::Manifest.new
50
+ m << Kube::Cluster["ConfigMap"].new {
51
+ metadata.name = "test"
52
+ }
53
+
54
+ stack = Middleware::Stack.new do
55
+ use Middleware::Namespace, "production"
56
+ end
57
+ stack.call(m)
58
+
59
+ resources = m.to_a
60
+ resources.first.to_h.dig(:metadata, :namespace).should == "production"
61
+ end
62
+
63
+ it "to_yaml_reflects_middleware" do
64
+ m = Kube::Cluster::Manifest.new
65
+ m << Kube::Cluster["ConfigMap"].new {
66
+ metadata.name = "test"
67
+ }
68
+
69
+ Middleware::Namespace.new("production").call(m)
70
+
71
+ yaml = m.to_yaml
72
+ yaml.should.include "namespace: production"
73
+ end
74
+
75
+ it "enumerable_methods_work" do
76
+ m = Kube::Cluster::Manifest.new
77
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "a" }
78
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "b" }
79
+
80
+ Middleware::Namespace.new("production").call(m)
81
+
82
+ names = m.map { |r| r.to_h.dig(:metadata, :name) }
83
+ names.should == %w[a b]
84
+ end
85
+
86
+ # ── Multi-middleware stack ──────────────────────────────────────────────
87
+
88
+ it "multiple_middleware_compose_in_order" do
89
+ m = Kube::Cluster::Manifest.new
90
+ m << Kube::Cluster["ConfigMap"].new {
91
+ metadata.name = "test"
92
+ }
93
+
94
+ stack = Middleware::Stack.new do
95
+ use Middleware::Namespace, "staging"
96
+ use Middleware::Labels, app: "myapp", managed_by: "kube_cluster"
97
+ end
98
+ stack.call(m)
99
+
100
+ r = m.first
101
+ h = r.to_h
102
+
103
+ h.dig(:metadata, :namespace).should == "staging"
104
+ end
105
+
106
+ # ── size reflects resource count ─────────────────────────────────────────
107
+
108
+ it "size_reflects_resource_count" do
109
+ m = Kube::Cluster::Manifest.new
110
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "a" }
111
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "b" }
112
+
113
+ m.size.should == 2
114
+ end
115
+
116
+ # ── each without block ──────────────────────────────────────────────────
117
+
118
+ it "each_without_block_returns_enumerator" do
119
+ m = Kube::Cluster::Manifest.new
120
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
121
+
122
+ Middleware::Namespace.new("production").call(m)
123
+
124
+ enum = m.each
125
+ enum.should.be.instance_of Enumerator
126
+ end
127
+
128
+ # ── Generative middleware produces new resources ─────────────────────────
129
+
130
+ it "generative_middleware_adds_service" do
131
+ m = Kube::Cluster::Manifest.new
132
+ m << Kube::Cluster["Deployment"].new {
133
+ metadata.name = "web"
134
+ metadata.namespace = "default"
135
+ spec.selector.matchLabels = { app: "web" }
136
+ spec.template.metadata.labels = { app: "web" }
137
+ spec.template.spec.containers = [
138
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
139
+ ]
140
+ }
141
+
142
+ Middleware::ServiceForDeployment.new.call(m)
143
+
144
+ kinds = m.map { |r| r.to_h[:kind] }
145
+ kinds.should == %w[Deployment Service]
146
+ end
147
+
148
+ it "generative_middleware_does_not_affect_non_matching_resources" do
149
+ m = Kube::Cluster::Manifest.new
150
+ m << Kube::Cluster["ConfigMap"].new {
151
+ metadata.name = "config"
152
+ }
153
+
154
+ Middleware::ServiceForDeployment.new.call(m)
155
+
156
+ resources = m.to_a
157
+ resources.size.should == 1
158
+ end
159
+
160
+ # ── Generated resources flow through subsequent middleware stages ────────
161
+
162
+ it "generated_resources_flow_through_subsequent_stages" do
163
+ m = Kube::Cluster::Manifest.new
164
+ m << Kube::Cluster["Deployment"].new {
165
+ metadata.name = "web"
166
+ spec.selector.matchLabels = { app: "web" }
167
+ spec.template.metadata.labels = { app: "web" }
168
+ spec.template.spec.containers = [
169
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
170
+ ]
171
+ }
172
+
173
+ stack = Middleware::Stack.new do
174
+ use Middleware::ServiceForDeployment # generates Service
175
+ use Middleware::Namespace, "production" # namespaces everything
176
+ use Middleware::Labels, managed_by: "middleware" # labels everything
177
+ end
178
+ stack.call(m)
179
+
180
+ resources = m.to_a
181
+ resources.size.should == 2
182
+ end
183
+
184
+ # ── YAML serializes integers correctly ──────────────────────────────────
185
+
186
+ it "to_yaml_serializes_integers_as_plain_values" do
187
+ m = Kube::Cluster::Manifest.new
188
+ m << Kube::Cluster["Deployment"].new {
189
+ metadata.name = "web"
190
+ spec.selector.matchLabels = { app: "web" }
191
+ spec.template.metadata.labels = { app: "web" }
192
+ spec.template.spec.containers = [
193
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
194
+ ]
195
+ }
196
+
197
+ yaml = m.to_yaml
198
+ yaml.should.include "containerPort: 8080"
199
+ end
200
+
201
+ # ── Multi-generative: chained generation ────────────────────────────────
202
+
203
+ it "chained_generative_middleware" do
204
+ m = Kube::Cluster::Manifest.new
205
+ m << Kube::Cluster["Deployment"].new {
206
+ metadata.name = "web"
207
+ metadata.namespace = "default"
208
+ metadata.labels = {
209
+ "app.kubernetes.io/expose": "app.example.com",
210
+ "app.kubernetes.io/autoscale": "2-10",
211
+ }
212
+ spec.selector.matchLabels = { app: "web" }
213
+ spec.template.metadata.labels = { app: "web" }
214
+ spec.template.spec.containers = [
215
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
216
+ ]
217
+ }
218
+
219
+ stack = Middleware::Stack.new do
220
+ use Middleware::ServiceForDeployment # Deployment → +Service
221
+ use Middleware::IngressForService # Service with expose label → +Ingress
222
+ use Middleware::HPAForDeployment # Deployment with autoscale label → +HPA
223
+ end
224
+ stack.call(m)
225
+
226
+ kinds = m.map { |r| r.to_h[:kind] }
227
+
228
+ m.to_a.size.should == 4
229
+ end
230
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "kube/cluster"
5
+
3
6
  module Kube
4
7
  module Cluster
5
8
  class Middleware
@@ -30,3 +33,61 @@ module Kube
30
33
  end
31
34
  end
32
35
  end
36
+
37
+ test do
38
+ Middleware = Kube::Cluster::Middleware
39
+
40
+ it "adds_annotations" do
41
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
42
+
43
+ Middleware::Annotations.new(
44
+ "prometheus.io/scrape": "true",
45
+ "prometheus.io/port": "9090",
46
+ ).call(m)
47
+
48
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
49
+
50
+ annotations[:"prometheus.io/port"].should == "9090"
51
+ end
52
+
53
+ it "resource_annotations_override_middleware_defaults" do
54
+ m = manifest(Kube::Cluster["ConfigMap"].new {
55
+ metadata.name = "test"
56
+ metadata.annotations = { "prometheus.io/port": "8080" }
57
+ })
58
+
59
+ Middleware::Annotations.new("prometheus.io/port": "9090").call(m)
60
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
61
+
62
+ annotations[:"prometheus.io/port"].should == "8080"
63
+ end
64
+
65
+ it "preserves_existing_annotations" do
66
+ m = manifest(Kube::Cluster["ConfigMap"].new {
67
+ metadata.name = "test"
68
+ metadata.annotations = { "custom/annotation": "keep" }
69
+ })
70
+
71
+ Middleware::Annotations.new("prometheus.io/scrape": "true").call(m)
72
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
73
+
74
+ annotations[:"prometheus.io/scrape"].should == "true"
75
+ end
76
+
77
+ it "converts_values_to_strings" do
78
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
79
+
80
+ Middleware::Annotations.new("prometheus.io/port": 9090).call(m)
81
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
82
+
83
+ annotations[:"prometheus.io/port"].should == "9090"
84
+ end
85
+
86
+ private
87
+
88
+ def manifest(*resources)
89
+ m = Kube::Cluster::Manifest.new
90
+ resources.each { |r| m << r }
91
+ m
92
+ end
93
+ end