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.
@@ -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
@@ -109,3 +112,160 @@ module Kube
109
112
  end
110
113
  end
111
114
  end
115
+
116
+ test do
117
+ Middleware = Kube::Cluster::Middleware
118
+
119
+ it "generates_hpa_from_deployment" do
120
+ m = manifest(Kube::Cluster["Deployment"].new {
121
+ metadata.name = "web"
122
+ metadata.namespace = "production"
123
+ metadata.labels = {
124
+ "app.kubernetes.io/name": "web",
125
+ "app.kubernetes.io/autoscale": "2-10",
126
+ }
127
+ spec.selector.matchLabels = { app: "web" }
128
+ spec.template.metadata.labels = { app: "web" }
129
+ spec.template.spec.containers = [
130
+ { name: "web", image: "nginx" },
131
+ ]
132
+ })
133
+
134
+ Middleware::HPAForDeployment.new.call(m)
135
+
136
+ deploy, hpa = m.resources
137
+ hh = hpa.to_h
138
+ metrics = hh.dig(:spec, :metrics)
139
+
140
+ metrics[1].dig(:resource, :target, :averageUtilization).should == 80
141
+ end
142
+
143
+ it "custom_cpu_and_memory_targets" do
144
+ m = manifest(Kube::Cluster["Deployment"].new {
145
+ metadata.name = "web"
146
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-3" }
147
+ spec.selector.matchLabels = { app: "web" }
148
+ spec.template.metadata.labels = { app: "web" }
149
+ spec.template.spec.containers = [
150
+ { name: "web", image: "nginx" },
151
+ ]
152
+ })
153
+
154
+ Middleware::HPAForDeployment.new(cpu: 60, memory: 70).call(m)
155
+ hpa = m.resources.last.to_h
156
+
157
+ hpa.dig(:spec, :metrics, 1, :resource, :target, :averageUtilization).should == 70
158
+ end
159
+
160
+ it "strips_autoscale_label_from_hpa" do
161
+ m = manifest(Kube::Cluster["Deployment"].new {
162
+ metadata.name = "web"
163
+ metadata.labels = {
164
+ "app.kubernetes.io/name": "web",
165
+ "app.kubernetes.io/autoscale": "1-5",
166
+ }
167
+ spec.selector.matchLabels = { app: "web" }
168
+ spec.template.metadata.labels = { app: "web" }
169
+ spec.template.spec.containers = [
170
+ { name: "web", image: "nginx" },
171
+ ]
172
+ })
173
+
174
+ Middleware::HPAForDeployment.new.call(m)
175
+ hpa_labels = m.resources.last.to_h.dig(:metadata, :labels)
176
+
177
+ hpa_labels[:"app.kubernetes.io/autoscale"].should.be.nil
178
+ end
179
+
180
+ it "skips_deployment_without_autoscale_label" do
181
+ m = manifest(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" },
187
+ ]
188
+ })
189
+
190
+ Middleware::HPAForDeployment.new.call(m)
191
+
192
+ m.resources.size.should == 1
193
+ end
194
+
195
+ it "skips_non_pod_bearing_resources" do
196
+ m = manifest(Kube::Cluster["ConfigMap"].new {
197
+ metadata.name = "config"
198
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-5" }
199
+ })
200
+
201
+ Middleware::HPAForDeployment.new.call(m)
202
+
203
+ m.resources.size.should == 1
204
+ end
205
+
206
+ it "raises_on_invalid_range_format" do
207
+ m = manifest(Kube::Cluster["Deployment"].new {
208
+ metadata.name = "web"
209
+ metadata.labels = { "app.kubernetes.io/autoscale": "bad" }
210
+ spec.selector.matchLabels = { app: "web" }
211
+ spec.template.metadata.labels = { app: "web" }
212
+ spec.template.spec.containers = [
213
+ { name: "web", image: "nginx" },
214
+ ]
215
+ })
216
+
217
+ error = nil
218
+ begin
219
+ Middleware::HPAForDeployment.new.call(m)
220
+ rescue ArgumentError => e
221
+ error = e
222
+ end
223
+
224
+ error.message.should.include "Invalid autoscale label"
225
+ end
226
+
227
+ it "raises_on_invalid_range_values" do
228
+ m = manifest(Kube::Cluster["Deployment"].new {
229
+ metadata.name = "web"
230
+ metadata.labels = { "app.kubernetes.io/autoscale": "5-2" }
231
+ spec.selector.matchLabels = { app: "web" }
232
+ spec.template.metadata.labels = { app: "web" }
233
+ spec.template.spec.containers = [
234
+ { name: "web", image: "nginx" },
235
+ ]
236
+ })
237
+
238
+ error = nil
239
+ begin
240
+ Middleware::HPAForDeployment.new.call(m)
241
+ rescue ArgumentError => e
242
+ error = e
243
+ end
244
+
245
+ error.message.should.include "Invalid autoscale range"
246
+ end
247
+
248
+ it "works_with_statefulset" do
249
+ m = manifest(Kube::Cluster["StatefulSet"].new {
250
+ metadata.name = "db"
251
+ metadata.labels = { "app.kubernetes.io/autoscale": "1-3" }
252
+ spec.selector.matchLabels = { app: "db" }
253
+ spec.template.metadata.labels = { app: "db" }
254
+ spec.template.spec.containers = [
255
+ { name: "postgres", image: "postgres:16" },
256
+ ]
257
+ })
258
+
259
+ Middleware::HPAForDeployment.new.call(m)
260
+
261
+ m.resources.last.to_h.dig(:spec, :scaleTargetRef, :kind).should == "StatefulSet"
262
+ end
263
+
264
+ private
265
+
266
+ def manifest(*resources)
267
+ m = Kube::Cluster::Manifest.new
268
+ resources.each { |r| m << r }
269
+ m
270
+ end
271
+ 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
@@ -89,3 +92,101 @@ module Kube
89
92
  end
90
93
  end
91
94
  end
95
+
96
+ test do
97
+ Middleware = Kube::Cluster::Middleware
98
+
99
+ it "generates_ingress_from_service_with_expose_label" do
100
+ m = manifest(Kube::Cluster["Service"].new {
101
+ metadata.name = "web"
102
+ metadata.namespace = "production"
103
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
104
+ spec.selector = { app: "web" }
105
+ spec.ports = [{ name: "http", port: 80, targetPort: "http" }]
106
+ })
107
+
108
+ Middleware::IngressForService.new.call(m)
109
+
110
+ service, ingress = m.resources
111
+ ih = ingress.to_h
112
+ rule = ih.dig(:spec, :rules, 0)
113
+
114
+ rule.dig(:http, :paths, 0, :backend, :service, :port, :name).should == "http"
115
+ end
116
+
117
+ it "custom_issuer_and_ingress_class" do
118
+ m = manifest(Kube::Cluster["Service"].new {
119
+ metadata.name = "web"
120
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
121
+ spec.ports = [{ name: "http", port: 80 }]
122
+ })
123
+
124
+ Middleware::IngressForService.new(
125
+ issuer: "letsencrypt-staging",
126
+ ingress_class: "traefik",
127
+ ).call(m)
128
+
129
+ ingress = m.resources.last.to_h
130
+
131
+ ingress.dig(:metadata, :annotations, :"cert-manager.io/cluster-issuer").should == "letsencrypt-staging"
132
+ end
133
+
134
+ it "expose_true_uses_name_as_hostname" do
135
+ m = manifest(Kube::Cluster["Service"].new {
136
+ metadata.name = "api"
137
+ metadata.labels = { "app.kubernetes.io/expose": "true" }
138
+ spec.ports = [{ name: "http", port: 80 }]
139
+ })
140
+
141
+ Middleware::IngressForService.new.call(m)
142
+ ingress = m.resources.last.to_h
143
+
144
+ ingress.dig(:spec, :tls, 0, :hosts).should == ["api.local"]
145
+ end
146
+
147
+ it "strips_expose_label_from_ingress" do
148
+ m = manifest(Kube::Cluster["Service"].new {
149
+ metadata.name = "web"
150
+ metadata.labels = {
151
+ "app.kubernetes.io/expose": "app.example.com",
152
+ "app.kubernetes.io/name": "web",
153
+ }
154
+ spec.ports = [{ name: "http", port: 80 }]
155
+ })
156
+
157
+ Middleware::IngressForService.new.call(m)
158
+ ingress_labels = m.resources.last.to_h.dig(:metadata, :labels)
159
+
160
+ ingress_labels[:"app.kubernetes.io/expose"].should.be.nil
161
+ end
162
+
163
+ it "skips_service_without_expose_label" do
164
+ m = manifest(Kube::Cluster["Service"].new {
165
+ metadata.name = "web"
166
+ spec.ports = [{ name: "http", port: 80 }]
167
+ })
168
+
169
+ Middleware::IngressForService.new.call(m)
170
+
171
+ m.resources.size.should == 1
172
+ end
173
+
174
+ it "skips_non_service_resources" do
175
+ m = manifest(Kube::Cluster["Deployment"].new {
176
+ metadata.name = "web"
177
+ metadata.labels = { "app.kubernetes.io/expose": "app.example.com" }
178
+ })
179
+
180
+ Middleware::IngressForService.new.call(m)
181
+
182
+ m.resources.size.should == 1
183
+ end
184
+
185
+ private
186
+
187
+ def manifest(*resources)
188
+ m = Kube::Cluster::Manifest.new
189
+ resources.each { |r| m << r }
190
+ m
191
+ end
192
+ 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
@@ -57,3 +60,83 @@ module Kube
57
60
  end
58
61
  end
59
62
  end
63
+
64
+ test do
65
+ Middleware = Kube::Cluster::Middleware
66
+
67
+ it "adds_standard_labels" do
68
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
69
+
70
+ Middleware::Labels.new(app: "web", managed_by: "kube_cluster").call(m)
71
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
72
+
73
+ labels[:"app.kubernetes.io/name"].should == "web"
74
+ end
75
+
76
+ it "maps_all_standard_keys" do
77
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
78
+
79
+ Middleware::Labels.new(
80
+ app: "web",
81
+ instance: "my-release",
82
+ version: "1.0.0",
83
+ component: "frontend",
84
+ part_of: "platform",
85
+ managed_by: "kube_cluster",
86
+ ).call(m)
87
+
88
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
89
+
90
+ labels[:"app.kubernetes.io/managed-by"].should == "kube_cluster"
91
+ end
92
+
93
+ it "resource_labels_override_middleware_defaults" do
94
+ m = manifest(Kube::Cluster["ConfigMap"].new {
95
+ metadata.name = "test"
96
+ metadata.labels = { "app.kubernetes.io/name": "override" }
97
+ })
98
+
99
+ Middleware::Labels.new(app: "default").call(m)
100
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
101
+
102
+ labels[:"app.kubernetes.io/name"].should == "override"
103
+ end
104
+
105
+ it "preserves_existing_labels" do
106
+ m = manifest(Kube::Cluster["ConfigMap"].new {
107
+ metadata.name = "test"
108
+ metadata.labels = { custom: "value" }
109
+ })
110
+
111
+ Middleware::Labels.new(app: "web").call(m)
112
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
113
+
114
+ labels[:custom].should == "value"
115
+ end
116
+
117
+ it "passes_through_non_standard_keys" do
118
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
119
+
120
+ Middleware::Labels.new(:"team.io/name" => "platform").call(m)
121
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
122
+
123
+ labels[:"team.io/name"].should == "platform"
124
+ end
125
+
126
+ it "converts_values_to_strings" do
127
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
128
+
129
+ Middleware::Labels.new(version: 2).call(m)
130
+ labels = m.resources.first.to_h.dig(:metadata, :labels)
131
+
132
+ labels[:"app.kubernetes.io/version"].should == "2"
133
+ end
134
+
135
+ private
136
+
137
+ def manifest(*resources)
138
+ m = Kube::Cluster::Manifest.new
139
+ resources.each { |r| m << r }
140
+ m
141
+ end
142
+ 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
@@ -29,3 +32,75 @@ module Kube
29
32
  end
30
33
  end
31
34
  end
35
+
36
+ test do
37
+ Middleware = Kube::Cluster::Middleware
38
+
39
+ it "sets_namespace_on_configmap" do
40
+ m = manifest(Kube::Cluster["ConfigMap"].new { metadata.name = "test" })
41
+
42
+ Middleware::Namespace.new("production").call(m)
43
+
44
+ m.resources.first.to_h.dig(:metadata, :namespace).should == "production"
45
+ end
46
+
47
+ it "sets_namespace_on_deployment" do
48
+ m = manifest(Kube::Cluster["Deployment"].new { metadata.name = "web" })
49
+
50
+ Middleware::Namespace.new("staging").call(m)
51
+
52
+ m.resources.first.to_h.dig(:metadata, :namespace).should == "staging"
53
+ end
54
+
55
+ it "skips_namespace_resource" do
56
+ m = manifest(Kube::Cluster["Namespace"].new { metadata.name = "my-ns" })
57
+
58
+ Middleware::Namespace.new("production").call(m)
59
+
60
+ m.resources.first.to_h.dig(:metadata, :namespace).should.be.nil
61
+ end
62
+
63
+ it "skips_cluster_role" do
64
+ m = manifest(Kube::Cluster["ClusterRole"].new { metadata.name = "admin" })
65
+
66
+ Middleware::Namespace.new("production").call(m)
67
+
68
+ m.resources.first.to_h.dig(:metadata, :namespace).should.be.nil
69
+ end
70
+
71
+ it "skips_cluster_role_binding" do
72
+ m = manifest(Kube::Cluster["ClusterRoleBinding"].new { metadata.name = "admin-binding" })
73
+
74
+ Middleware::Namespace.new("production").call(m)
75
+
76
+ m.resources.first.to_h.dig(:metadata, :namespace).should.be.nil
77
+ end
78
+
79
+ it "overwrites_existing_namespace" do
80
+ m = manifest(Kube::Cluster["ConfigMap"].new {
81
+ metadata.name = "test"
82
+ metadata.namespace = "old"
83
+ })
84
+
85
+ Middleware::Namespace.new("new").call(m)
86
+
87
+ m.resources.first.to_h.dig(:metadata, :namespace).should == "new"
88
+ end
89
+
90
+ it "returns_new_resource_instance" do
91
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "test" }
92
+ m = manifest(resource)
93
+
94
+ Middleware::Namespace.new("production").call(m)
95
+
96
+ resource.equal?(m.resources.first).should.be.false
97
+ end
98
+
99
+ private
100
+
101
+ def manifest(*resources)
102
+ m = Kube::Cluster::Manifest.new
103
+ resources.each { |r| m << r }
104
+ m
105
+ end
106
+ 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
@@ -59,3 +62,126 @@ module Kube
59
62
  end
60
63
  end
61
64
  end
65
+
66
+ test do
67
+ Middleware = Kube::Cluster::Middleware
68
+
69
+ it "injects_soft_anti_affinity_on_deployment" do
70
+ m = manifest(Kube::Cluster["Deployment"].new {
71
+ metadata.name = "web"
72
+ spec.selector.matchLabels = { app: "web", instance: "prod" }
73
+ spec.template.metadata.labels = { app: "web" }
74
+ spec.template.spec.containers = [
75
+ { name: "web", image: "nginx:latest" },
76
+ ]
77
+ })
78
+
79
+ Middleware::PodAntiAffinity.new.call(m)
80
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
81
+
82
+ paa = affinity.dig(:podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution)
83
+ paa.size.should == 1
84
+ end
85
+
86
+ it "custom_topology_key" do
87
+ m = manifest(Kube::Cluster["Deployment"].new {
88
+ metadata.name = "web"
89
+ spec.selector.matchLabels = { app: "web" }
90
+ spec.template.metadata.labels = { app: "web" }
91
+ spec.template.spec.containers = [
92
+ { name: "web", image: "nginx:latest" },
93
+ ]
94
+ })
95
+
96
+ Middleware::PodAntiAffinity.new(
97
+ topology_key: "topology.kubernetes.io/zone",
98
+ ).call(m)
99
+
100
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
101
+ term = affinity.dig(:podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution, 0)
102
+
103
+ term.dig(:podAffinityTerm, :topologyKey).should == "topology.kubernetes.io/zone"
104
+ end
105
+
106
+ it "custom_weight" do
107
+ m = manifest(Kube::Cluster["Deployment"].new {
108
+ metadata.name = "web"
109
+ spec.selector.matchLabels = { app: "web" }
110
+ spec.template.metadata.labels = { app: "web" }
111
+ spec.template.spec.containers = [
112
+ { name: "web", image: "nginx:latest" },
113
+ ]
114
+ })
115
+
116
+ Middleware::PodAntiAffinity.new(weight: 100).call(m)
117
+ term = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity,
118
+ :podAntiAffinity, :preferredDuringSchedulingIgnoredDuringExecution, 0)
119
+
120
+ term[:weight].should == 100
121
+ end
122
+
123
+ it "skips_resources_with_existing_affinity" do
124
+ m = manifest(Kube::Cluster["Deployment"].new {
125
+ metadata.name = "web"
126
+ spec.selector.matchLabels = { app: "web" }
127
+ spec.template.metadata.labels = { app: "web" }
128
+ spec.template.spec.affinity = { nodeAffinity: { custom: true } }
129
+ spec.template.spec.containers = [
130
+ { name: "web", image: "nginx:latest" },
131
+ ]
132
+ })
133
+
134
+ Middleware::PodAntiAffinity.new.call(m)
135
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
136
+
137
+ affinity.should == { nodeAffinity: { custom: true } }
138
+ end
139
+
140
+ it "skips_non_pod_bearing_resources" do
141
+ resource = Kube::Cluster["ConfigMap"].new { metadata.name = "config" }
142
+ m = manifest(resource)
143
+
144
+ Middleware::PodAntiAffinity.new.call(m)
145
+
146
+ m.resources.first.to_h.should == resource.to_h
147
+ end
148
+
149
+ it "skips_resources_without_match_labels" do
150
+ m = manifest(Kube::Cluster["Deployment"].new {
151
+ metadata.name = "web"
152
+ spec.template.metadata.labels = { app: "web" }
153
+ spec.template.spec.containers = [
154
+ { name: "web", image: "nginx:latest" },
155
+ ]
156
+ })
157
+
158
+ Middleware::PodAntiAffinity.new.call(m)
159
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
160
+
161
+ affinity.should.be.nil
162
+ end
163
+
164
+ it "applies_to_statefulset" do
165
+ m = manifest(Kube::Cluster["StatefulSet"].new {
166
+ metadata.name = "db"
167
+ spec.selector.matchLabels = { app: "db" }
168
+ spec.template.metadata.labels = { app: "db" }
169
+ spec.template.spec.containers = [
170
+ { name: "postgres", image: "postgres:16" },
171
+ ]
172
+ })
173
+
174
+ Middleware::PodAntiAffinity.new.call(m)
175
+ affinity = m.resources.first.to_h.dig(:spec, :template, :spec, :affinity)
176
+
177
+ affinity.dig(:podAntiAffinity).should.not.be.nil
178
+ end
179
+
180
+ private
181
+
182
+ def manifest(*resources)
183
+ m = Kube::Cluster::Manifest.new
184
+ resources.each { |r| m << r }
185
+ m
186
+ end
187
+ end