kube_cluster 0.3.7 → 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.
@@ -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
@@ -109,3 +114,180 @@ module Kube
109
114
  end
110
115
  end
111
116
  end
117
+
118
+ if __FILE__ == $0
119
+ require "minitest/autorun"
120
+
121
+ class HPAForDeploymentMiddlewareTest < Minitest::Test
122
+ Middleware = Kube::Cluster::Middleware
123
+
124
+ def test_generates_hpa_from_deployment
125
+ m = manifest(Kube::Cluster["Deployment"].new {
126
+ metadata.name = "web"
127
+ metadata.namespace = "production"
128
+ metadata.labels = {
129
+ "app.kubernetes.io/name": "web",
130
+ "app.kubernetes.io/autoscale": "2-10",
131
+ }
132
+ spec.selector.matchLabels = { app: "web" }
133
+ spec.template.metadata.labels = { app: "web" }
134
+ spec.template.spec.containers = [
135
+ { name: "web", image: "nginx" },
136
+ ]
137
+ })
138
+
139
+ Middleware::HPAForDeployment.new.call(m)
140
+
141
+ assert_equal 2, m.resources.size
142
+
143
+ deploy, hpa = m.resources
144
+ assert_equal "Deployment", deploy.to_h[:kind]
145
+ assert_equal "HorizontalPodAutoscaler", hpa.to_h[:kind]
146
+
147
+ hh = hpa.to_h
148
+ assert_equal "web", hh.dig(:metadata, :name)
149
+ assert_equal "production", hh.dig(:metadata, :namespace)
150
+ assert_equal 2, hh.dig(:spec, :minReplicas)
151
+ assert_equal 10, hh.dig(:spec, :maxReplicas)
152
+
153
+ ref = hh.dig(:spec, :scaleTargetRef)
154
+ assert_equal "apps/v1", ref[:apiVersion]
155
+ assert_equal "Deployment", ref[:kind]
156
+ assert_equal "web", ref[:name]
157
+
158
+ metrics = hh.dig(:spec, :metrics)
159
+ assert_equal 2, metrics.size
160
+ assert_equal "cpu", metrics[0].dig(:resource, :name)
161
+ assert_equal 75, metrics[0].dig(:resource, :target, :averageUtilization)
162
+ assert_equal "memory", metrics[1].dig(:resource, :name)
163
+ assert_equal 80, metrics[1].dig(:resource, :target, :averageUtilization)
164
+ end
165
+
166
+ def test_custom_cpu_and_memory_targets
167
+ m = manifest(Kube::Cluster["Deployment"].new {
168
+ metadata.name = "web"
169
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-3" }
170
+ spec.selector.matchLabels = { app: "web" }
171
+ spec.template.metadata.labels = { app: "web" }
172
+ spec.template.spec.containers = [
173
+ { name: "web", image: "nginx" },
174
+ ]
175
+ })
176
+
177
+ Middleware::HPAForDeployment.new(cpu: 60, memory: 70).call(m)
178
+ hpa = m.resources.last.to_h
179
+
180
+ assert_equal 60, hpa.dig(:spec, :metrics, 0, :resource, :target, :averageUtilization)
181
+ assert_equal 70, hpa.dig(:spec, :metrics, 1, :resource, :target, :averageUtilization)
182
+ end
183
+
184
+ def test_strips_autoscale_label_from_hpa
185
+ m = manifest(Kube::Cluster["Deployment"].new {
186
+ metadata.name = "web"
187
+ metadata.labels = {
188
+ "app.kubernetes.io/name": "web",
189
+ "app.kubernetes.io/autoscale": "1-5",
190
+ }
191
+ spec.selector.matchLabels = { app: "web" }
192
+ spec.template.metadata.labels = { app: "web" }
193
+ spec.template.spec.containers = [
194
+ { name: "web", image: "nginx" },
195
+ ]
196
+ })
197
+
198
+ Middleware::HPAForDeployment.new.call(m)
199
+ hpa_labels = m.resources.last.to_h.dig(:metadata, :labels)
200
+
201
+ assert_nil hpa_labels[:"app.kubernetes.io/autoscale"]
202
+ assert_equal "web", hpa_labels[:"app.kubernetes.io/name"]
203
+ end
204
+
205
+ def test_skips_deployment_without_autoscale_label
206
+ m = manifest(Kube::Cluster["Deployment"].new {
207
+ metadata.name = "web"
208
+ spec.selector.matchLabels = { app: "web" }
209
+ spec.template.metadata.labels = { app: "web" }
210
+ spec.template.spec.containers = [
211
+ { name: "web", image: "nginx" },
212
+ ]
213
+ })
214
+
215
+ Middleware::HPAForDeployment.new.call(m)
216
+
217
+ assert_equal 1, m.resources.size
218
+ end
219
+
220
+ def test_skips_non_pod_bearing_resources
221
+ m = manifest(Kube::Cluster["ConfigMap"].new {
222
+ metadata.name = "config"
223
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-5" }
224
+ })
225
+
226
+ Middleware::HPAForDeployment.new.call(m)
227
+
228
+ assert_equal 1, m.resources.size
229
+ end
230
+
231
+ def test_raises_on_invalid_range_format
232
+ m = manifest(Kube::Cluster["Deployment"].new {
233
+ metadata.name = "web"
234
+ metadata.labels = { "app.kubernetes.io/autoscale": "bad" }
235
+ spec.selector.matchLabels = { app: "web" }
236
+ spec.template.metadata.labels = { app: "web" }
237
+ spec.template.spec.containers = [
238
+ { name: "web", image: "nginx" },
239
+ ]
240
+ })
241
+
242
+ error = assert_raises(ArgumentError) do
243
+ Middleware::HPAForDeployment.new.call(m)
244
+ end
245
+
246
+ assert_includes error.message, "Invalid autoscale label"
247
+ end
248
+
249
+ def test_raises_on_invalid_range_values
250
+ m = manifest(Kube::Cluster["Deployment"].new {
251
+ metadata.name = "web"
252
+ metadata.labels = { "app.kubernetes.io/autoscale": "5-2" }
253
+ spec.selector.matchLabels = { app: "web" }
254
+ spec.template.metadata.labels = { app: "web" }
255
+ spec.template.spec.containers = [
256
+ { name: "web", image: "nginx" },
257
+ ]
258
+ })
259
+
260
+ error = assert_raises(ArgumentError) do
261
+ Middleware::HPAForDeployment.new.call(m)
262
+ end
263
+
264
+ assert_includes error.message, "Invalid autoscale range"
265
+ end
266
+
267
+ def test_works_with_statefulset
268
+ m = manifest(Kube::Cluster["StatefulSet"].new {
269
+ metadata.name = "db"
270
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-3" }
271
+ spec.selector.matchLabels = { app: "db" }
272
+ spec.template.metadata.labels = { app: "db" }
273
+ spec.template.spec.containers = [
274
+ { name: "postgres", image: "postgres:16" },
275
+ ]
276
+ })
277
+
278
+ Middleware::HPAForDeployment.new.call(m)
279
+
280
+ assert_equal 2, m.resources.size
281
+ hpa = m.resources.last.to_h
282
+ assert_equal "StatefulSet", hpa.dig(:spec, :scaleTargetRef, :kind)
283
+ end
284
+
285
+ private
286
+
287
+ def manifest(*resources)
288
+ m = Kube::Cluster::Manifest.new
289
+ resources.each { |r| m << r }
290
+ m
291
+ end
292
+ end
293
+ 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
@@ -89,3 +94,125 @@ module Kube
89
94
  end
90
95
  end
91
96
  end
97
+
98
+ if __FILE__ == $0
99
+ require "minitest/autorun"
100
+
101
+ class IngressForServiceMiddlewareTest < Minitest::Test
102
+ Middleware = Kube::Cluster::Middleware
103
+
104
+ def test_generates_ingress_from_service_with_expose_label
105
+ m = manifest(Kube::Cluster["Service"].new {
106
+ metadata.name = "web"
107
+ metadata.namespace = "production"
108
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
109
+ spec.selector = { app: "web" }
110
+ spec.ports = [{ name: "http", port: 80, targetPort: "http" }]
111
+ })
112
+
113
+ Middleware::IngressForService.new.call(m)
114
+
115
+ assert_equal 2, m.resources.size
116
+
117
+ service, ingress = m.resources
118
+ assert_equal "Service", service.to_h[:kind]
119
+ assert_equal "Ingress", ingress.to_h[:kind]
120
+
121
+ ih = ingress.to_h
122
+ assert_equal "web", ih.dig(:metadata, :name)
123
+ assert_equal "production", ih.dig(:metadata, :namespace)
124
+ assert_equal "nginx", ih.dig(:spec, :ingressClassName)
125
+ assert_equal "letsencrypt-prod", ih.dig(:metadata, :annotations, :"cert-manager.io/cluster-issuer")
126
+ assert_equal "true", ih.dig(:metadata, :annotations, :"nginx.ingress.kubernetes.io/ssl-redirect")
127
+
128
+ # TLS
129
+ tls = ih.dig(:spec, :tls, 0)
130
+ assert_equal ["app.example.com"], tls[:hosts]
131
+ assert_equal "web-tls", tls[:secretName]
132
+
133
+ # Rules
134
+ rule = ih.dig(:spec, :rules, 0)
135
+ assert_equal "app.example.com", rule[:host]
136
+ assert_equal "web", rule.dig(:http, :paths, 0, :backend, :service, :name)
137
+ assert_equal "http", rule.dig(:http, :paths, 0, :backend, :service, :port, :name)
138
+ end
139
+
140
+ def test_custom_issuer_and_ingress_class
141
+ m = manifest(Kube::Cluster["Service"].new {
142
+ metadata.name = "web"
143
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
144
+ spec.ports = [{ name: "http", port: 80 }]
145
+ })
146
+
147
+ Middleware::IngressForService.new(
148
+ issuer: "letsencrypt-staging",
149
+ ingress_class: "traefik",
150
+ ).call(m)
151
+
152
+ ingress = m.resources.last.to_h
153
+ assert_equal "traefik", ingress.dig(:spec, :ingressClassName)
154
+ assert_equal "letsencrypt-staging", ingress.dig(:metadata, :annotations, :"cert-manager.io/cluster-issuer")
155
+ end
156
+
157
+ def test_expose_true_uses_name_as_hostname
158
+ m = manifest(Kube::Cluster["Service"].new {
159
+ metadata.name = "api"
160
+ metadata.labels = { "app.kubernetes.io/expose": "true" }
161
+ spec.ports = [{ name: "http", port: 80 }]
162
+ })
163
+
164
+ Middleware::IngressForService.new.call(m)
165
+ ingress = m.resources.last.to_h
166
+
167
+ assert_equal "api.local", ingress.dig(:spec, :rules, 0, :host)
168
+ assert_equal ["api.local"], ingress.dig(:spec, :tls, 0, :hosts)
169
+ end
170
+
171
+ def test_strips_expose_label_from_ingress
172
+ m = manifest(Kube::Cluster["Service"].new {
173
+ metadata.name = "web"
174
+ metadata.labels = {
175
+ "app.kubernetes.io/expose": "app.example.com",
176
+ "app.kubernetes.io/name": "web",
177
+ }
178
+ spec.ports = [{ name: "http", port: 80 }]
179
+ })
180
+
181
+ Middleware::IngressForService.new.call(m)
182
+ ingress_labels = m.resources.last.to_h.dig(:metadata, :labels)
183
+
184
+ assert_nil ingress_labels[:"app.kubernetes.io/expose"]
185
+ assert_equal "web", ingress_labels[:"app.kubernetes.io/name"]
186
+ end
187
+
188
+ def test_skips_service_without_expose_label
189
+ m = manifest(Kube::Cluster["Service"].new {
190
+ metadata.name = "web"
191
+ spec.ports = [{ name: "http", port: 80 }]
192
+ })
193
+
194
+ Middleware::IngressForService.new.call(m)
195
+
196
+ assert_equal 1, m.resources.size
197
+ end
198
+
199
+ def test_skips_non_service_resources
200
+ m = manifest(Kube::Cluster["Deployment"].new {
201
+ metadata.name = "web"
202
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
203
+ })
204
+
205
+ Middleware::IngressForService.new.call(m)
206
+
207
+ assert_equal 1, m.resources.size
208
+ end
209
+
210
+ private
211
+
212
+ def manifest(*resources)
213
+ m = Kube::Cluster::Manifest.new
214
+ resources.each { |r| m << r }
215
+ m
216
+ end
217
+ end
218
+ 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
@@ -57,3 +62,94 @@ module Kube
57
62
  end
58
63
  end
59
64
  end
65
+
66
+ if __FILE__ == $0
67
+ require "minitest/autorun"
68
+
69
+ class LabelsMiddlewareTest < Minitest::Test
70
+ Middleware = Kube::Cluster::Middleware
71
+
72
+ def test_adds_standard_labels
73
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
74
+
75
+ Middleware::Labels.new(app: "web", managed_by: "kube_cluster").call(m)
76
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
77
+
78
+ assert_equal "web", labels[:"app.kubernetes.io/name"]
79
+ assert_equal "kube_cluster", labels[:"app.kubernetes.io/managed-by"]
80
+ end
81
+
82
+ def test_maps_all_standard_keys
83
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
84
+
85
+ Middleware::Labels.new(
86
+ app: "web",
87
+ instance: "my-release",
88
+ version: "1.0.0",
89
+ component: "frontend",
90
+ part_of: "platform",
91
+ managed_by: "kube_cluster",
92
+ ).call(m)
93
+
94
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
95
+
96
+ assert_equal "web", labels[:"app.kubernetes.io/name"]
97
+ assert_equal "my-release", labels[:"app.kubernetes.io/instance"]
98
+ assert_equal "1.0.0", labels[:"app.kubernetes.io/version"]
99
+ assert_equal "frontend", labels[:"app.kubernetes.io/component"]
100
+ assert_equal "platform", labels[:"app.kubernetes.io/part-of"]
101
+ assert_equal "kube_cluster", labels[:"app.kubernetes.io/managed-by"]
102
+ end
103
+
104
+ def test_resource_labels_override_middleware_defaults
105
+ m = manifest(Kube::Cluster["ConfigMap"].new {
106
+ metadata.name = "test"
107
+ metadata.labels = { "app.kubernetes.io/name": "override" }
108
+ })
109
+
110
+ Middleware::Labels.new(app: "default").call(m)
111
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
112
+
113
+ assert_equal "override", labels[:"app.kubernetes.io/name"]
114
+ end
115
+
116
+ def test_preserves_existing_labels
117
+ m = manifest(Kube::Cluster["ConfigMap"].new {
118
+ metadata.name = "test"
119
+ metadata.labels = { custom: "value" }
120
+ })
121
+
122
+ Middleware::Labels.new(app: "web").call(m)
123
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
124
+
125
+ assert_equal "value", labels[:custom]
126
+ assert_equal "web", labels[:"app.kubernetes.io/name"]
127
+ end
128
+
129
+ def test_passes_through_non_standard_keys
130
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
131
+
132
+ Middleware::Labels.new(:"team.io/name" => "platform").call(m)
133
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
134
+
135
+ assert_equal "platform", labels[:"team.io/name"]
136
+ end
137
+
138
+ def test_converts_values_to_strings
139
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
140
+
141
+ Middleware::Labels.new(version: 2).call(m)
142
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
143
+
144
+ assert_equal "2", labels[:"app.kubernetes.io/version"]
145
+ end
146
+
147
+ private
148
+
149
+ def manifest(*resources)
150
+ m = Kube::Cluster::Manifest.new
151
+ resources.each { |r| m << r }
152
+ m
153
+ end
154
+ end
155
+ 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
@@ -29,3 +34,79 @@ module Kube
29
34
  end
30
35
  end
31
36
  end
37
+
38
+ if __FILE__ == $0
39
+ require "minitest/autorun"
40
+
41
+ class NamespaceMiddlewareTest < Minitest::Test
42
+ Middleware = Kube::Cluster::Middleware
43
+
44
+ def test_sets_namespace_on_configmap
45
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
46
+
47
+ Middleware::Namespace.new("production").call(m)
48
+
49
+ assert_equal "production", m.resources.first.to_h.dig(:metadata, :namespace)
50
+ end
51
+
52
+ def test_sets_namespace_on_deployment
53
+ m = manifest(Kube::Cluster["Deployment"].new { metadata.name = "web" })
54
+
55
+ Middleware::Namespace.new("staging").call(m)
56
+
57
+ assert_equal "staging", m.resources.first.to_h.dig(:metadata, :namespace)
58
+ end
59
+
60
+ def test_skips_namespace_resource
61
+ m = manifest(Kube::Cluster["Namespace"].new { metadata.name = "my-ns" })
62
+
63
+ Middleware::Namespace.new("production").call(m)
64
+
65
+ assert_nil m.resources.first.to_h.dig(:metadata, :namespace)
66
+ end
67
+
68
+ def test_skips_cluster_role
69
+ m = manifest(Kube::Cluster["ClusterRole"].new { metadata.name = "admin" })
70
+
71
+ Middleware::Namespace.new("production").call(m)
72
+
73
+ assert_nil m.resources.first.to_h.dig(:metadata, :namespace)
74
+ end
75
+
76
+ def test_skips_cluster_role_binding
77
+ m = manifest(Kube::Cluster["ClusterRoleBinding"].new { metadata.name = "admin-binding" })
78
+
79
+ Middleware::Namespace.new("production").call(m)
80
+
81
+ assert_nil m.resources.first.to_h.dig(:metadata, :namespace)
82
+ end
83
+
84
+ def test_overwrites_existing_namespace
85
+ m = manifest(Kube::Cluster["ConfigMap"].new {
86
+ metadata.name = "test"
87
+ metadata.namespace = "old"
88
+ })
89
+
90
+ Middleware::Namespace.new("new").call(m)
91
+
92
+ assert_equal "new", m.resources.first.to_h.dig(:metadata, :namespace)
93
+ end
94
+
95
+ def test_returns_new_resource_instance
96
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
97
+ m = manifest(resource)
98
+
99
+ Middleware::Namespace.new("production").call(m)
100
+
101
+ refute_same resource, m.resources.first
102
+ end
103
+
104
+ private
105
+
106
+ def manifest(*resources)
107
+ m = Kube::Cluster::Manifest.new
108
+ resources.each { |r| m << r }
109
+ m
110
+ end
111
+ end
112
+ 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
@@ -59,3 +64,135 @@ module Kube
59
64
  end
60
65
  end
61
66
  end
67
+
68
+ if __FILE__ == $0
69
+ require "minitest/autorun"
70
+
71
+ class PodAntiAffinityMiddlewareTest < Minitest::Test
72
+ Middleware = Kube::Cluster::Middleware
73
+
74
+ def test_injects_soft_anti_affinity_on_deployment
75
+ m = manifest(Kube::Cluster["Deployment"].new {
76
+ metadata.name = "web"
77
+ spec.selector.matchLabels = { app: "web", instance: "prod" }
78
+ spec.template.metadata.labels = { app: "web" }
79
+ spec.template.spec.containers = [
80
+ { name: "web", image: "nginx:latest" },
81
+ ]
82
+ })
83
+
84
+ Middleware::PodAntiAffinity.new.call(m)
85
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
86
+
87
+ paa = affinity.dig(:podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution)
88
+ assert_equal 1, paa.size
89
+
90
+ term = paa.first
91
+ assert_equal 1, term[:weight]
92
+ assert_equal "kubernetes.io/hostname", term.dig(:podAffinityTerm, :topologyKey)
93
+ assert_equal({ app: "web", instance: "prod" }, term.dig(:podAffinityTerm, :labelSelector, :matchLabels))
94
+ end
95
+
96
+ def test_custom_topology_key
97
+ m = manifest(Kube::Cluster["Deployment"].new {
98
+ metadata.name = "web"
99
+ spec.selector.matchLabels = { app: "web" }
100
+ spec.template.metadata.labels = { app: "web" }
101
+ spec.template.spec.containers = [
102
+ { name: "web", image: "nginx:latest" },
103
+ ]
104
+ })
105
+
106
+ Middleware::PodAntiAffinity.new(
107
+ topology_key: "topology.kubernetes.io/zone",
108
+ ).call(m)
109
+
110
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
111
+ term = affinity.dig(:podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution, 0)
112
+
113
+ assert_equal "topology.kubernetes.io/zone", term.dig(:podAffinityTerm, :topologyKey)
114
+ end
115
+
116
+ def test_custom_weight
117
+ m = manifest(Kube::Cluster["Deployment"].new {
118
+ metadata.name = "web"
119
+ spec.selector.matchLabels = { app: "web" }
120
+ spec.template.metadata.labels = { app: "web" }
121
+ spec.template.spec.containers = [
122
+ { name: "web", image: "nginx:latest" },
123
+ ]
124
+ })
125
+
126
+ Middleware::PodAntiAffinity.new(weight: 100).call(m)
127
+ term = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity,
128
+ :podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution, 0)
129
+
130
+ assert_equal 100, term[:weight]
131
+ end
132
+
133
+ def test_skips_resources_with_existing_affinity
134
+ m = manifest(Kube::Cluster["Deployment"].new {
135
+ metadata.name = "web"
136
+ spec.selector.matchLabels = { app: "web" }
137
+ spec.template.metadata.labels = { app: "web" }
138
+ spec.template.spec.affinity = { nodeAffinity: { custom: true } }
139
+ spec.template.spec.containers = [
140
+ { name: "web", image: "nginx:latest" },
141
+ ]
142
+ })
143
+
144
+ Middleware::PodAntiAffinity.new.call(m)
145
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
146
+
147
+ assert_equal({ nodeAffinity: { custom: true } }, affinity)
148
+ end
149
+
150
+ def test_skips_non_pod_bearing_resources
151
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "config" }
152
+ m = manifest(resource)
153
+
154
+ Middleware::PodAntiAffinity.new.call(m)
155
+
156
+ assert_equal resource.to_h, m.resources.first.to_h
157
+ end
158
+
159
+ def test_skips_resources_without_match_labels
160
+ m = manifest(Kube::Cluster["Deployment"].new {
161
+ metadata.name = "web"
162
+ spec.template.metadata.labels = { app: "web" }
163
+ spec.template.spec.containers = [
164
+ { name: "web", image: "nginx:latest" },
165
+ ]
166
+ })
167
+
168
+ Middleware::PodAntiAffinity.new.call(m)
169
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
170
+
171
+ assert_nil affinity
172
+ end
173
+
174
+ def test_applies_to_statefulset
175
+ m = manifest(Kube::Cluster["StatefulSet"].new {
176
+ metadata.name = "db"
177
+ spec.selector.matchLabels = { app: "db" }
178
+ spec.template.metadata.labels = { app: "db" }
179
+ spec.template.spec.containers = [
180
+ { name: "postgres", image: "postgres:16" },
181
+ ]
182
+ })
183
+
184
+ Middleware::PodAntiAffinity.new.call(m)
185
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
186
+
187
+ refute_nil affinity.dig(:podAntiAffinity)
188
+ end
189
+
190
+ private
191
+
192
+ def manifest(*resources)
193
+ m = Kube::Cluster::Manifest.new
194
+ resources.each { |r| m << r }
195
+ m
196
+ end
197
+ end
198
+ end