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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -5
- data/README.md +133 -10
- data/Rakefile +7 -0
- data/bin/test +8 -6
- data/kube_cluster.gemspec +1 -1
- data/lib/kube/cluster/manifest.rb +238 -0
- data/lib/kube/cluster/middleware/annotations.rb +69 -0
- data/lib/kube/cluster/middleware/hpa_for_deployment.rb +182 -0
- data/lib/kube/cluster/middleware/ingress_for_service.rb +127 -0
- data/lib/kube/cluster/middleware/labels.rb +96 -0
- data/lib/kube/cluster/middleware/namespace.rb +81 -0
- data/lib/kube/cluster/middleware/pod_anti_affinity.rb +137 -0
- data/lib/kube/cluster/middleware/resource_preset.rb +188 -0
- data/lib/kube/cluster/middleware/security_context.rb +170 -0
- data/lib/kube/cluster/middleware/service_for_deployment.rb +167 -0
- data/lib/kube/cluster/resource/dirty_tracking.rb +625 -0
- data/lib/kube/cluster/version.rb +1 -1
- data/lib/kube/cluster.rb +11 -0
- data/lib/kube/helm/chart.rb +410 -0
- data/lib/kube/helm/endpoint.rb +86 -0
- data/lib/kube/helm/repo.rb +203 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abe22aedfb5ac2ca4451e036aaa97364c551cdb6ff795bd05057f47311cd801b
|
|
4
|
+
data.tar.gz: 77300dffe8e8189dc95996ed080283e94ba316ae894fa36e4a94bc9a18045e68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
5
|
-
kube_kubectl (~> 2.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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3
|
+
Ruby-native Kubernetes. Define, transform, and deploy cluster resources with pure Ruby.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
|
|
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
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("
|
|
7
|
+
files = Dir.glob("lib/**/*.rb").sort.select { |f|
|
|
8
|
+
File.read(f).include?("if __FILE__ == $0")
|
|
9
|
+
}
|
|
12
10
|
else
|
|
13
|
-
|
|
11
|
+
files = ARGV
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
files.each do |f|
|
|
15
|
+
system("bundle", "exec", "ruby", f, exception: true)
|
|
14
16
|
end
|
data/kube_cluster.gemspec
CHANGED
|
@@ -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
|