kube_cluster 0.3.6 → 0.3.8

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: 986f42db2ebf9220a0f11d2a328b87fee963967850c876982616f7382bd6874f
4
- data.tar.gz: 68e3af397e254a19258fc53e5cf2c827eedc32b6f125707a9dfb523b2bfea986
3
+ metadata.gz: abe22aedfb5ac2ca4451e036aaa97364c551cdb6ff795bd05057f47311cd801b
4
+ data.tar.gz: 77300dffe8e8189dc95996ed080283e94ba316ae894fa36e4a94bc9a18045e68
5
5
  SHA512:
6
- metadata.gz: 057db75ecbc9e8b37787590e79175a451e099b91c23689d0a622d9bbe4da718d8607eb419815048f53fc8cdc532ec79f4883dc92689e87975bbd24528a87e833
7
- data.tar.gz: 43632e98c87d9da5e0ff163a43bdebf8990dbbfec1478cad4f9a89d33cdd7b5cebb0f88e185d29917a521e0afda5703c23126e0e851fe91c44b2ca4a03c7c19e
6
+ metadata.gz: 5d2b36b773ada5eb5ec794125f825155c6910cf737e67bcbcd9c6f40ec0508e5752fb9744a78d94344ad7f42b7acf19c38ab2ac55c6c148a9eac57fcac0499fe
7
+ data.tar.gz: 668b0c45f7b9229eb4476f0d536545afa1e060e8b970f8ae968dc9dfef39fda93df5786b9f804e7a31d284fb7e4d2adbeeb8f7d7ff5c9199233e0e6a6c08588d
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_cluster (0.3.6)
5
- kube_kubectl (~> 2.0.0)
4
+ kube_cluster (0.3.8)
5
+ kube_kubectl (~> 2.0.9)
6
6
  kube_schema (~> 1.3.0)
7
7
 
8
8
  GEM
@@ -28,12 +28,12 @@ GEM
28
28
  hana (~> 1.3)
29
29
  regexp_parser (~> 2.0)
30
30
  simpleidn (~> 0.2)
31
- kube_kubectl (2.0.8)
31
+ kube_kubectl (2.0.9)
32
32
  debug (~> 1.11)
33
33
  json_schemer (~> 2.5)
34
34
  rubyshell (~> 1.5)
35
35
  shellwords (~> 0.2.2)
36
- string_builder (~> 1.2.0)
36
+ string_builder (~> 1.2.2)
37
37
  kube_schema (1.3.4)
38
38
  json_schemer (~> 2.5.0)
39
39
  rubyshell (~> 1.5.0)
@@ -79,7 +79,7 @@ GEM
79
79
  rubyshell (1.5.0)
80
80
  shellwords (0.2.2)
81
81
  simpleidn (0.2.3)
82
- string_builder (1.2.1)
82
+ string_builder (1.2.2)
83
83
  stringio (3.2.0)
84
84
  tsort (0.2.0)
85
85
  unicode-display_width (3.2.0)
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
+ Dir["lib/**/*.rb"].each { |f| sh "ruby", f }
5
+ end
6
+
7
+ task default: :test
data/bin/test CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- $LOAD_PATH.unshift File.expand_path("../test", __dir__)
5
-
6
- require "bundler/setup"
7
-
8
4
  Dir.chdir(File.expand_path("..", __dir__))
9
5
 
10
6
  if ARGV.empty?
11
- Dir.glob("test/**/*_test.rb").sort.each { |f| require_relative "../#{f}" }
7
+ files = Dir.glob("lib/**/*.rb").sort.select { |f|
8
+ File.read(f).include?("if __FILE__ == $0")
9
+ }
12
10
  else
13
- ARGV.each { |f| require_relative "../#{f}" }
11
+ files = ARGV
12
+ end
13
+
14
+ files.each do |f|
15
+ system("bundle", "exec", "ruby", f, exception: true)
14
16
  end
@@ -0,0 +1,63 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ bigdecimal (4.1.2)
5
+ date (3.5.1)
6
+ debug (1.11.1)
7
+ irb (~> 1.10)
8
+ reline (>= 0.3.8)
9
+ erb (6.0.3)
10
+ hana (1.3.7)
11
+ io-console (0.8.2)
12
+ irb (1.17.0)
13
+ pp (>= 0.6.0)
14
+ prism (>= 1.3.0)
15
+ rdoc (>= 4.0.0)
16
+ reline (>= 0.4.2)
17
+ json_schemer (2.5.0)
18
+ bigdecimal
19
+ hana (~> 1.3)
20
+ regexp_parser (~> 2.0)
21
+ simpleidn (~> 0.2)
22
+ kube_cluster (0.3.5)
23
+ kube_kubectl (~> 2.0.0)
24
+ kube_schema (~> 1.3.0)
25
+ kube_kubectl (2.0.8)
26
+ debug (~> 1.11)
27
+ json_schemer (~> 2.5)
28
+ rubyshell (~> 1.5)
29
+ shellwords (~> 0.2.2)
30
+ string_builder (~> 1.2.0)
31
+ kube_schema (1.3.4)
32
+ json_schemer (~> 2.5.0)
33
+ rubyshell (~> 1.5.0)
34
+ pp (0.6.3)
35
+ prettyprint
36
+ prettyprint (0.2.0)
37
+ prism (1.9.0)
38
+ psych (5.3.1)
39
+ date
40
+ stringio
41
+ rdoc (7.2.0)
42
+ erb
43
+ psych (>= 4.0.0)
44
+ tsort
45
+ regexp_parser (2.12.0)
46
+ reline (0.6.3)
47
+ io-console (~> 0.5)
48
+ rubyshell (1.5.0)
49
+ shellwords (0.2.2)
50
+ simpleidn (0.2.3)
51
+ string_builder (1.2.1)
52
+ stringio (3.2.0)
53
+ tsort (0.2.0)
54
+
55
+ PLATFORMS
56
+ ruby
57
+ x86_64-linux
58
+
59
+ DEPENDENCIES
60
+ kube_cluster (~> 0.3.2)
61
+
62
+ BUNDLED WITH
63
+ 2.6.9
@@ -0,0 +1,63 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ bigdecimal (4.1.2)
5
+ date (3.5.1)
6
+ debug (1.11.1)
7
+ irb (~> 1.10)
8
+ reline (>= 0.3.8)
9
+ erb (6.0.3)
10
+ hana (1.3.7)
11
+ io-console (0.8.2)
12
+ irb (1.17.0)
13
+ pp (>= 0.6.0)
14
+ prism (>= 1.3.0)
15
+ rdoc (>= 4.0.0)
16
+ reline (>= 0.4.2)
17
+ json_schemer (2.5.0)
18
+ bigdecimal
19
+ hana (~> 1.3)
20
+ regexp_parser (~> 2.0)
21
+ simpleidn (~> 0.2)
22
+ kube_cluster (0.3.5)
23
+ kube_kubectl (~> 2.0.0)
24
+ kube_schema (~> 1.3.0)
25
+ kube_kubectl (2.0.8)
26
+ debug (~> 1.11)
27
+ json_schemer (~> 2.5)
28
+ rubyshell (~> 1.5)
29
+ shellwords (~> 0.2.2)
30
+ string_builder (~> 1.2.0)
31
+ kube_schema (1.3.4)
32
+ json_schemer (~> 2.5.0)
33
+ rubyshell (~> 1.5.0)
34
+ pp (0.6.3)
35
+ prettyprint
36
+ prettyprint (0.2.0)
37
+ prism (1.9.0)
38
+ psych (5.3.1)
39
+ date
40
+ stringio
41
+ rdoc (7.2.0)
42
+ erb
43
+ psych (>= 4.0.0)
44
+ tsort
45
+ regexp_parser (2.12.0)
46
+ reline (0.6.3)
47
+ io-console (~> 0.5)
48
+ rubyshell (1.5.0)
49
+ shellwords (0.2.2)
50
+ simpleidn (0.2.3)
51
+ string_builder (1.2.1)
52
+ stringio (3.2.0)
53
+ tsort (0.2.0)
54
+
55
+ PLATFORMS
56
+ ruby
57
+ x86_64-linux
58
+
59
+ DEPENDENCIES
60
+ kube_cluster (~> 0.3.2)
61
+
62
+ BUNDLED WITH
63
+ 2.6.9
data/kube_cluster.gemspec CHANGED
@@ -33,5 +33,5 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "rubocop", "~> 1.21"
34
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_kubectl", "~> 2.0.9"
37
37
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if __FILE__ == $0
4
+ require "bundler/setup"
5
+ require "kube/cluster"
6
+ end
7
+
3
8
  module Kube
4
9
  module Cluster
5
10
  # A flat, ordered collection of Kubernetes resources.
@@ -23,3 +28,236 @@ module Kube
23
28
  end
24
29
  end
25
30
  end
31
+
32
+ if __FILE__ == $0
33
+ require "minitest/autorun"
34
+
35
+ class ManifestTest < Minitest::Test
36
+ Middleware = Kube::Cluster::Middleware
37
+
38
+ # ── Bare manifest ────────────────────────────────────────────────────────
39
+
40
+ def test_bare_manifest_enumerates_resources_unchanged
41
+ m = Kube::Cluster::Manifest.new
42
+ m << Kube::Cluster["ConfigMap"].new {
43
+ metadata.name = "test"
44
+ self.data = { key: "value" }
45
+ }
46
+
47
+ resources = m.to_a
48
+ assert_equal 1, resources.size
49
+ assert_equal "ConfigMap", resources.first.to_h[:kind]
50
+ assert_equal "test", resources.first.to_h.dig(:metadata, :name)
51
+ end
52
+
53
+ # ── Stack transforms resources ───────────────────────────────────────────
54
+
55
+ def test_stack_transforms_resources
56
+ m = Kube::Cluster::Manifest.new
57
+ m << Kube::Cluster["ConfigMap"].new {
58
+ metadata.name = "test"
59
+ }
60
+
61
+ stack = Middleware::Stack.new do
62
+ use Middleware::Namespace, "production"
63
+ end
64
+ stack.call(m)
65
+
66
+ resources = m.to_a
67
+ assert_equal "production", resources.first.to_h.dig(:metadata, :namespace)
68
+ end
69
+
70
+ def test_to_yaml_reflects_middleware
71
+ m = Kube::Cluster::Manifest.new
72
+ m << Kube::Cluster["ConfigMap"].new {
73
+ metadata.name = "test"
74
+ }
75
+
76
+ Middleware::Namespace.new("production").call(m)
77
+
78
+ yaml = m.to_yaml
79
+ assert_includes yaml, "namespace: production"
80
+ end
81
+
82
+ def test_enumerable_methods_work
83
+ m = Kube::Cluster::Manifest.new
84
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "a" }
85
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "b" }
86
+
87
+ Middleware::Namespace.new("production").call(m)
88
+
89
+ names = m.map { |r| r.to_h.dig(:metadata, :name) }
90
+ assert_equal %w[a b], names
91
+
92
+ all_namespaced = m.all? { |r| r.to_h.dig(:metadata, :namespace) == "production" }
93
+ assert all_namespaced
94
+ end
95
+
96
+ # ── Multi-middleware stack ──────────────────────────────────────────────
97
+
98
+ def test_multiple_middleware_compose_in_order
99
+ m = Kube::Cluster::Manifest.new
100
+ m << Kube::Cluster["ConfigMap"].new {
101
+ metadata.name = "test"
102
+ }
103
+
104
+ stack = Middleware::Stack.new do
105
+ use Middleware::Namespace, "staging"
106
+ use Middleware::Labels, app: "myapp", managed_by: "kube_cluster"
107
+ end
108
+ stack.call(m)
109
+
110
+ r = m.first
111
+ h = r.to_h
112
+
113
+ assert_equal "staging", h.dig(:metadata, :namespace)
114
+ assert_equal "myapp", h.dig(:metadata, :labels, :"app.kubernetes.io/name")
115
+ assert_equal "kube_cluster", h.dig(:metadata, :labels, :"app.kubernetes.io/managed-by")
116
+ end
117
+
118
+ # ── size reflects resource count ─────────────────────────────────────────
119
+
120
+ def test_size_reflects_resource_count
121
+ m = Kube::Cluster::Manifest.new
122
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "a" }
123
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "b" }
124
+
125
+ assert_equal 2, m.size
126
+ assert_equal 2, m.length
127
+ end
128
+
129
+ # ── each without block ──────────────────────────────────────────────────
130
+
131
+ def test_each_without_block_returns_enumerator
132
+ m = Kube::Cluster::Manifest.new
133
+ m << Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
134
+
135
+ Middleware::Namespace.new("production").call(m)
136
+
137
+ enum = m.each
138
+ assert_instance_of Enumerator, enum
139
+
140
+ r = enum.first
141
+ assert_equal "production", r.to_h.dig(:metadata, :namespace)
142
+ end
143
+
144
+ # ── Generative middleware produces new resources ─────────────────────────
145
+
146
+ def test_generative_middleware_adds_service
147
+ m = Kube::Cluster::Manifest.new
148
+ m << Kube::Cluster["Deployment"].new {
149
+ metadata.name = "web"
150
+ metadata.namespace = "default"
151
+ spec.selector.matchLabels = { app: "web" }
152
+ spec.template.metadata.labels = { app: "web" }
153
+ spec.template.spec.containers = [
154
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
155
+ ]
156
+ }
157
+
158
+ Middleware::ServiceForDeployment.new.call(m)
159
+
160
+ kinds = m.map { |r| r.to_h[:kind] }
161
+ assert_equal %w[Deployment Service], kinds
162
+ end
163
+
164
+ def test_generative_middleware_does_not_affect_non_matching_resources
165
+ m = Kube::Cluster::Manifest.new
166
+ m << Kube::Cluster["ConfigMap"].new {
167
+ metadata.name = "config"
168
+ }
169
+
170
+ Middleware::ServiceForDeployment.new.call(m)
171
+
172
+ resources = m.to_a
173
+ assert_equal 1, resources.size
174
+ assert_equal "ConfigMap", resources.first.to_h[:kind]
175
+ end
176
+
177
+ # ── Generated resources flow through subsequent middleware stages ────────
178
+
179
+ def test_generated_resources_flow_through_subsequent_stages
180
+ m = Kube::Cluster::Manifest.new
181
+ m << Kube::Cluster["Deployment"].new {
182
+ metadata.name = "web"
183
+ spec.selector.matchLabels = { app: "web" }
184
+ spec.template.metadata.labels = { app: "web" }
185
+ spec.template.spec.containers = [
186
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
187
+ ]
188
+ }
189
+
190
+ stack = Middleware::Stack.new do
191
+ use Middleware::ServiceForDeployment # generates Service
192
+ use Middleware::Namespace, "production" # namespaces everything
193
+ use Middleware::Labels, managed_by: "middleware" # labels everything
194
+ end
195
+ stack.call(m)
196
+
197
+ resources = m.to_a
198
+ assert_equal 2, resources.size
199
+
200
+ # Both the Deployment and the generated Service got namespaced and labeled
201
+ resources.each do |r|
202
+ h = r.to_h
203
+ assert_equal "production", h.dig(:metadata, :namespace),
204
+ "Expected #{h[:kind]} to be namespaced"
205
+ assert_equal "middleware", h.dig(:metadata, :labels, :"app.kubernetes.io/managed-by"),
206
+ "Expected #{h[:kind]} to be labeled"
207
+ end
208
+ end
209
+
210
+ # ── YAML serializes integers correctly ──────────────────────────────────
211
+
212
+ def test_to_yaml_serializes_integers_as_plain_values
213
+ m = Kube::Cluster::Manifest.new
214
+ m << Kube::Cluster["Deployment"].new {
215
+ metadata.name = "web"
216
+ spec.selector.matchLabels = { app: "web" }
217
+ spec.template.metadata.labels = { app: "web" }
218
+ spec.template.spec.containers = [
219
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
220
+ ]
221
+ }
222
+
223
+ yaml = m.to_yaml
224
+ refute_includes yaml, "!ruby/object:Integer",
225
+ "Integer values must serialize as plain YAML numbers, not !ruby/object:Integer"
226
+ assert_includes yaml, "containerPort: 8080"
227
+ end
228
+
229
+ # ── Multi-generative: chained generation ────────────────────────────────
230
+
231
+ def test_chained_generative_middleware
232
+ m = Kube::Cluster::Manifest.new
233
+ m << Kube::Cluster["Deployment"].new {
234
+ metadata.name = "web"
235
+ metadata.namespace = "default"
236
+ metadata.labels = {
237
+ "app.kubernetes.io/expose": "app.example.com",
238
+ "app.kubernetes.io/autoscale": "2-10",
239
+ }
240
+ spec.selector.matchLabels = { app: "web" }
241
+ spec.template.metadata.labels = { app: "web" }
242
+ spec.template.spec.containers = [
243
+ { name: "web", image: "nginx", ports: [{ name: "http", containerPort: 8080 }] },
244
+ ]
245
+ }
246
+
247
+ stack = Middleware::Stack.new do
248
+ use Middleware::ServiceForDeployment # Deployment → +Service
249
+ use Middleware::IngressForService # Service with expose label → +Ingress
250
+ use Middleware::HPAForDeployment # Deployment with autoscale label → +HPA
251
+ end
252
+ stack.call(m)
253
+
254
+ kinds = m.map { |r| r.to_h[:kind] }
255
+
256
+ assert_includes kinds, "Deployment"
257
+ assert_includes kinds, "Service"
258
+ assert_includes kinds, "Ingress"
259
+ assert_includes kinds, "HorizontalPodAutoscaler"
260
+ assert_equal 4, m.to_a.size
261
+ end
262
+ end
263
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if __FILE__ == $0
4
+ require "bundler/setup"
5
+ require "kube/cluster"
6
+ end
7
+
3
8
  module Kube
4
9
  module Cluster
5
10
  class Middleware
@@ -30,3 +35,67 @@ module Kube
30
35
  end
31
36
  end
32
37
  end
38
+
39
+ if __FILE__ == $0
40
+ require "minitest/autorun"
41
+
42
+ class AnnotationsMiddlewareTest < Minitest::Test
43
+ Middleware = Kube::Cluster::Middleware
44
+
45
+ def test_adds_annotations
46
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
47
+
48
+ Middleware::Annotations.new(
49
+ "prometheus.io/scrape": "true",
50
+ "prometheus.io/port": "9090",
51
+ ).call(m)
52
+
53
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
54
+
55
+ assert_equal "true", annotations[:"prometheus.io/scrape"]
56
+ assert_equal "9090", annotations[:"prometheus.io/port"]
57
+ end
58
+
59
+ def test_resource_annotations_override_middleware_defaults
60
+ m = manifest(Kube::Cluster["ConfigMap"].new {
61
+ metadata.name = "test"
62
+ metadata.annotations = { "prometheus.io/port": "8080" }
63
+ })
64
+
65
+ Middleware::Annotations.new("prometheus.io/port": "9090").call(m)
66
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
67
+
68
+ assert_equal "8080", annotations[:"prometheus.io/port"]
69
+ end
70
+
71
+ def test_preserves_existing_annotations
72
+ m = manifest(Kube::Cluster["ConfigMap"].new {
73
+ metadata.name = "test"
74
+ metadata.annotations = { "custom/annotation": "keep" }
75
+ })
76
+
77
+ Middleware::Annotations.new("prometheus.io/scrape": "true").call(m)
78
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
79
+
80
+ assert_equal "keep", annotations[:"custom/annotation"]
81
+ assert_equal "true", annotations[:"prometheus.io/scrape"]
82
+ end
83
+
84
+ def test_converts_values_to_strings
85
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
86
+
87
+ Middleware::Annotations.new("prometheus.io/port": 9090).call(m)
88
+ annotations = m.resources.first.to_h.dig(:metadata, :annotations)
89
+
90
+ assert_equal "9090", annotations[:"prometheus.io/port"]
91
+ end
92
+
93
+ private
94
+
95
+ def manifest(*resources)
96
+ m = Kube::Cluster::Manifest.new
97
+ resources.each { |r| m << r }
98
+ m
99
+ end
100
+ end
101
+ end