kube_schema 1.3.9 → 1.4.0
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 +1 -1
- data/bin/copy-schemas-over +2 -0
- data/bin/create-kubevirt-x-values +54 -0
- data/examples/sub_specs.rb +412 -0
- data/lib/kube/schema/instance.rb +106 -0
- data/lib/kube/schema/resource.rb +2 -0
- data/lib/kube/schema/sub_spec.rb +485 -0
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +1 -0
- data/schemas/kubevirt-definitions.json +143 -143
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f145d4e219013c8c1f0091ce99d7a2a10d1700201e77c6ed2d3d26db140206b0
|
|
4
|
+
data.tar.gz: 0114343af9ef19eb9f7496ed0506859126cef3912bb593c855c36565550dd504
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd5ee4e5f81546204cc2ff1040131a3cb09f4d512a4230adcaab86f7a2db0c65d0ec3034fc6613461f8de2eb549ea7c2e297da6e3de5ddc107f23629afe93727
|
|
7
|
+
data.tar.gz: cf4e3bf4b4a913ccc5108fe42c274c350d7d09db67fdc195028f6f0ddd513d82fa0799674d453cdfd7f3e7c086739c4150175cd7c62b5b7ab5ffca90e8b32b3f
|
data/Gemfile.lock
CHANGED
data/bin/copy-schemas-over
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Rewrites schemas/kubevirt-definitions.json in-place to:
|
|
4
|
+
# 1. Add x-kubernetes-group-version-kind annotations
|
|
5
|
+
# 2. Fix $ref pointers from k8s.io.* to io.k8s.* prefix
|
|
6
|
+
#
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
DEFS_FILE="schemas/kubevirt-definitions.json"
|
|
10
|
+
|
|
11
|
+
echo "Rewriting $DEFS_FILE..."
|
|
12
|
+
|
|
13
|
+
jq '
|
|
14
|
+
# GVK map: definition key -> [{ group, version, kind }]
|
|
15
|
+
{
|
|
16
|
+
"v1.KubeVirt": [{ group: "kubevirt.io", version: "v1", kind: "KubeVirt" }],
|
|
17
|
+
"v1.VirtualMachine": [{ group: "kubevirt.io", version: "v1", kind: "VirtualMachine" }],
|
|
18
|
+
"v1.VirtualMachineExport": [{ group: "export.kubevirt.io", version: "v1", kind: "VirtualMachineExport" }],
|
|
19
|
+
"v1.VirtualMachineInstance": [{ group: "kubevirt.io", version: "v1", kind: "VirtualMachineInstance" }],
|
|
20
|
+
"v1.VirtualMachineInstanceMigration": [{ group: "kubevirt.io", version: "v1", kind: "VirtualMachineInstanceMigration" }],
|
|
21
|
+
"v1.VirtualMachineInstancePreset": [{ group: "kubevirt.io", version: "v1", kind: "VirtualMachineInstancePreset" }],
|
|
22
|
+
"v1.VirtualMachineInstanceReplicaSet": [{ group: "kubevirt.io", version: "v1", kind: "VirtualMachineInstanceReplicaSet" }],
|
|
23
|
+
"v1alpha1.MigrationPolicy": [{ group: "migrations.kubevirt.io", version: "v1alpha1", kind: "MigrationPolicy" }],
|
|
24
|
+
"v1alpha1.VirtualMachineBackup": [{ group: "backup.kubevirt.io", version: "v1alpha1", kind: "VirtualMachineBackup" }],
|
|
25
|
+
"v1beta1.VirtualMachineClone": [{ group: "clone.kubevirt.io", version: "v1beta1", kind: "VirtualMachineClone" }],
|
|
26
|
+
"v1beta1.VirtualMachineClusterInstancetype":[{ group: "instancetype.kubevirt.io", version: "v1beta1", kind: "VirtualMachineClusterInstancetype" }],
|
|
27
|
+
"v1beta1.VirtualMachineClusterPreference": [{ group: "instancetype.kubevirt.io", version: "v1beta1", kind: "VirtualMachineClusterPreference" }],
|
|
28
|
+
"v1beta1.VirtualMachineInstancetype": [{ group: "instancetype.kubevirt.io", version: "v1beta1", kind: "VirtualMachineInstancetype" }],
|
|
29
|
+
"v1beta1.VirtualMachinePool": [{ group: "pool.kubevirt.io", version: "v1beta1", kind: "VirtualMachinePool" }],
|
|
30
|
+
"v1beta1.VirtualMachinePreference": [{ group: "instancetype.kubevirt.io", version: "v1beta1", kind: "VirtualMachinePreference" }],
|
|
31
|
+
"v1beta1.VirtualMachineRestore": [{ group: "snapshot.kubevirt.io", version: "v1beta1", kind: "VirtualMachineRestore" }],
|
|
32
|
+
"v1beta1.VirtualMachineSnapshot": [{ group: "snapshot.kubevirt.io", version: "v1beta1", kind: "VirtualMachineSnapshot" }],
|
|
33
|
+
"v1beta1.VirtualMachineSnapshotContent": [{ group: "snapshot.kubevirt.io", version: "v1beta1", kind: "VirtualMachineSnapshotContent" }]
|
|
34
|
+
} as $gvk_map |
|
|
35
|
+
|
|
36
|
+
# Inject x-kubernetes-group-version-kind annotations
|
|
37
|
+
with_entries(
|
|
38
|
+
if $gvk_map[.key] then
|
|
39
|
+
.value["x-kubernetes-group-version-kind"] = $gvk_map[.key]
|
|
40
|
+
else . end
|
|
41
|
+
) |
|
|
42
|
+
|
|
43
|
+
# Fix $ref pointers: kubevirt uses k8s.io.* but base schema uses io.k8s.*
|
|
44
|
+
walk(
|
|
45
|
+
if type == "string" and startswith("#/definitions/k8s.io.")
|
|
46
|
+
then sub("^#/definitions/k8s\\.io\\."; "#/definitions/io.k8s.")
|
|
47
|
+
else . end
|
|
48
|
+
)
|
|
49
|
+
' "$DEFS_FILE" > "${DEFS_FILE}.tmp" && mv "${DEFS_FILE}.tmp" "$DEFS_FILE"
|
|
50
|
+
|
|
51
|
+
ANNOTATED=$(jq '[.[] | select(has("x-kubernetes-group-version-kind"))] | length' "$DEFS_FILE")
|
|
52
|
+
TOTAL=$(jq 'length' "$DEFS_FILE")
|
|
53
|
+
STALE_REFS=$(jq -r '[.. | strings | select(startswith("#/definitions/k8s.io."))] | length' "$DEFS_FILE")
|
|
54
|
+
echo "Done. $ANNOTATED/$TOTAL definitions annotated. $STALE_REFS stale k8s.io refs remaining."
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demonstrates Kube::Schema::SubSpec — typed, schema-validated wrappers
|
|
5
|
+
# for non-resource Kubernetes definitions (Container, Volume, Probe, etc.).
|
|
6
|
+
#
|
|
7
|
+
# SubSpec objects validate against the OpenAPI JSON Schema and auto-coerce
|
|
8
|
+
# to plain hashes when placed inside Resource specs.
|
|
9
|
+
#
|
|
10
|
+
# Run: bundle exec ruby examples/sub_specs.rb
|
|
11
|
+
|
|
12
|
+
require_relative "../lib/kube/schema"
|
|
13
|
+
|
|
14
|
+
SubSpec = Kube::Schema::SubSpec
|
|
15
|
+
Manifest = Kube::Schema::Manifest
|
|
16
|
+
|
|
17
|
+
puts "=" * 60
|
|
18
|
+
puts "Kube::Schema::SubSpec Examples"
|
|
19
|
+
puts "=" * 60
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ══════════════════════════════════════════════════════════════
|
|
24
|
+
# 1. CONTAINER — the core use case
|
|
25
|
+
# ══════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
app = SubSpec["Container"].new {
|
|
28
|
+
self.name = "app"
|
|
29
|
+
self.image = "myapp:1.4.2"
|
|
30
|
+
self.imagePullPolicy = "IfNotPresent"
|
|
31
|
+
self.command = ["/usr/bin/myapp"]
|
|
32
|
+
self.args = ["--config", "/etc/myapp/config.yaml"]
|
|
33
|
+
self.ports = [
|
|
34
|
+
{ name: "http", containerPort: 8080, protocol: "TCP" },
|
|
35
|
+
{ name: "metrics", containerPort: 9090, protocol: "TCP" }
|
|
36
|
+
]
|
|
37
|
+
self.env = [
|
|
38
|
+
{ name: "LOG_LEVEL", value: "info" },
|
|
39
|
+
{ name: "WORKERS", value: "4" },
|
|
40
|
+
{ name: "DB_PASSWORD", valueFrom: { secretKeyRef: { name: "db-creds", key: "password" } } }
|
|
41
|
+
]
|
|
42
|
+
self.resources.requests = { cpu: "100m", memory: "128Mi" }
|
|
43
|
+
self.resources.limits = { cpu: "500m", memory: "256Mi" }
|
|
44
|
+
self.volumeMounts = [
|
|
45
|
+
{ name: "config", mountPath: "/etc/myapp", readOnly: true },
|
|
46
|
+
{ name: "data", mountPath: "/var/data" }
|
|
47
|
+
]
|
|
48
|
+
self.livenessProbe = {
|
|
49
|
+
httpGet: { path: "/healthz", port: 8080 },
|
|
50
|
+
initialDelaySeconds: 10,
|
|
51
|
+
periodSeconds: 30
|
|
52
|
+
}
|
|
53
|
+
self.readinessProbe = {
|
|
54
|
+
httpGet: { path: "/ready", port: 8080 },
|
|
55
|
+
initialDelaySeconds: 5,
|
|
56
|
+
periodSeconds: 10
|
|
57
|
+
}
|
|
58
|
+
self.securityContext.runAsNonRoot = true
|
|
59
|
+
self.securityContext.readOnlyRootFilesystem = true
|
|
60
|
+
self.securityContext.allowPrivilegeEscalation = false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
puts "1. Container: #{app.name}"
|
|
64
|
+
puts " image: #{app.image}"
|
|
65
|
+
puts " ports: #{app.ports.map { |p| p[:name] }.join(", ")}"
|
|
66
|
+
puts " valid? #{app.valid?}"
|
|
67
|
+
puts
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ══════════════════════════════════════════════════════════════
|
|
71
|
+
# 2. CONTAINER PORT — typed port definitions
|
|
72
|
+
# ══════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
http_port = SubSpec["ContainerPort"].new {
|
|
75
|
+
self.name = "http"
|
|
76
|
+
self.containerPort = 8080
|
|
77
|
+
self.protocol = "TCP"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
grpc_port = SubSpec["ContainerPort"].new(
|
|
81
|
+
name: "grpc", containerPort: 9090, protocol: "TCP"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
puts "2. ContainerPorts:"
|
|
85
|
+
puts " - #{http_port.name}: #{http_port.containerPort}"
|
|
86
|
+
puts " - #{grpc_port.name}: #{grpc_port.containerPort}"
|
|
87
|
+
puts
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ══════════════════════════════════════════════════════════════
|
|
91
|
+
# 3. ENV VAR — plain values and valueFrom references
|
|
92
|
+
# ══════════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
env_plain = SubSpec["EnvVar"].new(name: "APP_ENV", value: "production")
|
|
95
|
+
|
|
96
|
+
env_secret = SubSpec["EnvVar"].new {
|
|
97
|
+
self.name = "DB_PASSWORD"
|
|
98
|
+
self.valueFrom.secretKeyRef = { name: "db-creds", key: "password" }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
env_configmap = SubSpec["EnvVar"].new {
|
|
102
|
+
self.name = "LOG_FORMAT"
|
|
103
|
+
self.valueFrom.configMapKeyRef = { name: "app-config", key: "log_format" }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
env_field = SubSpec["EnvVar"].new {
|
|
107
|
+
self.name = "POD_NAME"
|
|
108
|
+
self.valueFrom.fieldRef = { fieldPath: "metadata.name" }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
puts "3. EnvVars:"
|
|
112
|
+
[env_plain, env_secret, env_configmap, env_field].each do |e|
|
|
113
|
+
puts " - #{e.name}: valid? #{e.valid?}"
|
|
114
|
+
end
|
|
115
|
+
puts
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ══════════════════════════════════════════════════════════════
|
|
119
|
+
# 4. VOLUME + VOLUME MOUNT — paired definitions
|
|
120
|
+
# ══════════════════════════════════════════════════════════════
|
|
121
|
+
|
|
122
|
+
config_volume = SubSpec["Volume"].new {
|
|
123
|
+
self.name = "config"
|
|
124
|
+
self.configMap = { name: "app-config" }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
secret_volume = SubSpec["Volume"].new {
|
|
128
|
+
self.name = "tls-certs"
|
|
129
|
+
self.secret = { secretName: "tls-secret" }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
data_volume = SubSpec["Volume"].new {
|
|
133
|
+
self.name = "data"
|
|
134
|
+
self.emptyDir = { sizeLimit: "1Gi" }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pvc_volume = SubSpec["Volume"].new(
|
|
138
|
+
name: "storage",
|
|
139
|
+
persistentVolumeClaim: { claimName: "app-pvc" }
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
config_mount = SubSpec["VolumeMount"].new(
|
|
143
|
+
name: "config", mountPath: "/etc/app", readOnly: true
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
tls_mount = SubSpec["VolumeMount"].new {
|
|
147
|
+
self.name = "tls-certs"
|
|
148
|
+
self.mountPath = "/etc/tls"
|
|
149
|
+
self.readOnly = true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
data_mount = SubSpec["VolumeMount"].new(
|
|
153
|
+
name: "data", mountPath: "/var/data"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
puts "4. Volumes:"
|
|
157
|
+
[config_volume, secret_volume, data_volume, pvc_volume].each do |v|
|
|
158
|
+
puts " - #{v.name}: valid? #{v.valid?}"
|
|
159
|
+
end
|
|
160
|
+
puts " Mounts:"
|
|
161
|
+
[config_mount, tls_mount, data_mount].each do |m|
|
|
162
|
+
puts " - #{m.name} -> #{m.mountPath}"
|
|
163
|
+
end
|
|
164
|
+
puts
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ══════════════════════════════════════════════════════════════
|
|
168
|
+
# 5. PROBE — liveness, readiness, startup
|
|
169
|
+
# ══════════════════════════════════════════════════════════════
|
|
170
|
+
|
|
171
|
+
http_probe = SubSpec["Probe"].new {
|
|
172
|
+
self.httpGet = { path: "/healthz", port: 8080 }
|
|
173
|
+
self.initialDelaySeconds = 15
|
|
174
|
+
self.periodSeconds = 20
|
|
175
|
+
self.timeoutSeconds = 3
|
|
176
|
+
self.failureThreshold = 3
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
tcp_probe = SubSpec["Probe"].new {
|
|
180
|
+
self.tcpSocket = { port: 5432 }
|
|
181
|
+
self.initialDelaySeconds = 5
|
|
182
|
+
self.periodSeconds = 10
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
exec_probe = SubSpec["Probe"].new {
|
|
186
|
+
self.exec = { command: ["/bin/sh", "-c", "pg_isready -U postgres"] }
|
|
187
|
+
self.initialDelaySeconds = 10
|
|
188
|
+
self.periodSeconds = 30
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
grpc_probe = SubSpec["Probe"].new {
|
|
192
|
+
self.grpc = { port: 50051 }
|
|
193
|
+
self.periodSeconds = 10
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
puts "5. Probes:"
|
|
197
|
+
puts " - HTTP: valid? #{http_probe.valid?}"
|
|
198
|
+
puts " - TCP: valid? #{tcp_probe.valid?}"
|
|
199
|
+
puts " - Exec: valid? #{exec_probe.valid?}"
|
|
200
|
+
puts " - gRPC: valid? #{grpc_probe.valid?}"
|
|
201
|
+
puts
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ══════════════════════════════════════════════════════════════
|
|
205
|
+
# 6. SECURITY CONTEXT — hardened container
|
|
206
|
+
# ══════════════════════════════════════════════════════════════
|
|
207
|
+
|
|
208
|
+
hardened = SubSpec["SecurityContext"].new {
|
|
209
|
+
self.runAsNonRoot = true
|
|
210
|
+
self.runAsUser = 1000
|
|
211
|
+
self.runAsGroup = 1000
|
|
212
|
+
self.readOnlyRootFilesystem = true
|
|
213
|
+
self.allowPrivilegeEscalation = false
|
|
214
|
+
self.capabilities = { drop: ["ALL"] }
|
|
215
|
+
self.seccompProfile = { type: "RuntimeDefault" }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
puts "6. SecurityContext:"
|
|
219
|
+
puts " runAsUser: #{hardened.runAsUser}"
|
|
220
|
+
puts " readOnlyRootFilesystem: #{hardened.readOnlyRootFilesystem}"
|
|
221
|
+
puts " valid? #{hardened.valid?}"
|
|
222
|
+
puts
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ══════════════════════════════════════════════════════════════
|
|
226
|
+
# 7. TOLERATION + TOPOLOGY SPREAD CONSTRAINT
|
|
227
|
+
# ══════════════════════════════════════════════════════════════
|
|
228
|
+
|
|
229
|
+
gpu_toleration = SubSpec["Toleration"].new(
|
|
230
|
+
key: "nvidia.com/gpu", operator: "Exists", effect: "NoSchedule"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
spot_toleration = SubSpec["Toleration"].new {
|
|
234
|
+
self.key = "kubernetes.azure.com/scalesetpriority"
|
|
235
|
+
self.operator = "Equal"
|
|
236
|
+
self.value = "spot"
|
|
237
|
+
self.effect = "NoSchedule"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
zone_spread = SubSpec["TopologySpreadConstraint"].new {
|
|
241
|
+
self.maxSkew = 1
|
|
242
|
+
self.topologyKey = "topology.kubernetes.io/zone"
|
|
243
|
+
self.whenUnsatisfiable = "DoNotSchedule"
|
|
244
|
+
self.labelSelector = { matchLabels: { app: "web" } }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
puts "7. Scheduling:"
|
|
248
|
+
puts " Tolerations:"
|
|
249
|
+
puts " - #{gpu_toleration.key}: valid? #{gpu_toleration.valid?}"
|
|
250
|
+
puts " - #{spot_toleration.key}: valid? #{spot_toleration.valid?}"
|
|
251
|
+
puts " TopologySpreadConstraint:"
|
|
252
|
+
puts " - #{zone_spread.topologyKey}: valid? #{zone_spread.valid?}"
|
|
253
|
+
puts
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ══════════════════════════════════════════════════════════════
|
|
257
|
+
# 8. SERVICE PORT — for Service specs
|
|
258
|
+
# ══════════════════════════════════════════════════════════════
|
|
259
|
+
|
|
260
|
+
svc_http = SubSpec["ServicePort"].new(
|
|
261
|
+
name: "http", port: 80, targetPort: 8080, protocol: "TCP"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
svc_https = SubSpec["ServicePort"].new {
|
|
265
|
+
self.name = "https"
|
|
266
|
+
self.port = 443
|
|
267
|
+
self.targetPort = 8443
|
|
268
|
+
self.protocol = "TCP"
|
|
269
|
+
self.appProtocol = "kubernetes.io/h2c"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
puts "8. ServicePorts:"
|
|
273
|
+
puts " - #{svc_http.name}: #{svc_http.port} -> #{svc_http.targetPort}"
|
|
274
|
+
puts " - #{svc_https.name}: #{svc_https.port} -> #{svc_https.targetPort}"
|
|
275
|
+
puts
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ══════════════════════════════════════════════════════════════
|
|
279
|
+
# 9. COMPOSING A FULL DEPLOYMENT — tying it all together
|
|
280
|
+
# ══════════════════════════════════════════════════════════════
|
|
281
|
+
|
|
282
|
+
# Build typed sub-specs
|
|
283
|
+
web_container = SubSpec["Container"].new {
|
|
284
|
+
self.name = "web"
|
|
285
|
+
self.image = "nginx:1.27-alpine"
|
|
286
|
+
self.ports = [http_port.to_h]
|
|
287
|
+
self.resources.requests = { cpu: "50m", memory: "64Mi" }
|
|
288
|
+
self.resources.limits = { cpu: "200m", memory: "128Mi" }
|
|
289
|
+
self.volumeMounts = [config_mount.to_h, tls_mount.to_h]
|
|
290
|
+
self.livenessProbe = http_probe.to_h
|
|
291
|
+
self.readinessProbe = http_probe.to_h
|
|
292
|
+
self.securityContext = hardened.to_h
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
sidecar = SubSpec["Container"].new {
|
|
296
|
+
self.name = "log-shipper"
|
|
297
|
+
self.image = "fluent/fluent-bit:3.2"
|
|
298
|
+
self.resources.requests = { cpu: "25m", memory: "32Mi" }
|
|
299
|
+
self.resources.limits = { cpu: "100m", memory: "64Mi" }
|
|
300
|
+
self.volumeMounts = [data_mount.to_h]
|
|
301
|
+
self.securityContext = hardened.to_h
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Compose into a Deployment — SubSpec instances auto-coerce
|
|
305
|
+
deployment = Kube::Schema["Deployment"].new {
|
|
306
|
+
metadata.name = "web"
|
|
307
|
+
metadata.namespace = "production"
|
|
308
|
+
metadata.labels = { app: "web", tier: "frontend" }
|
|
309
|
+
|
|
310
|
+
spec.replicas = 3
|
|
311
|
+
spec.selector.matchLabels = { app: "web" }
|
|
312
|
+
spec.strategy.type = "RollingUpdate"
|
|
313
|
+
spec.strategy.rollingUpdate = { maxSurge: 1, maxUnavailable: 0 }
|
|
314
|
+
|
|
315
|
+
spec.template.metadata.labels = { app: "web", tier: "frontend" }
|
|
316
|
+
|
|
317
|
+
# Auto-coercion: SubSpec instances become plain hashes automatically
|
|
318
|
+
spec.template.spec.containers = [web_container, sidecar]
|
|
319
|
+
spec.template.spec.volumes = [config_volume, secret_volume, data_volume]
|
|
320
|
+
|
|
321
|
+
spec.template.spec.tolerations = [spot_toleration]
|
|
322
|
+
spec.template.spec.topologySpreadConstraints = [zone_spread]
|
|
323
|
+
|
|
324
|
+
spec.template.spec.securityContext.fsGroup = 1000
|
|
325
|
+
spec.template.spec.securityContext.runAsGroup = 1000
|
|
326
|
+
spec.template.spec.serviceAccountName = "web"
|
|
327
|
+
spec.template.spec.terminationGracePeriodSeconds = 30
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Also compose a Service using typed ServicePorts
|
|
331
|
+
service = Kube::Schema["Service"].new {
|
|
332
|
+
metadata.name = "web"
|
|
333
|
+
metadata.namespace = "production"
|
|
334
|
+
metadata.labels = { app: "web" }
|
|
335
|
+
spec.selector = { app: "web" }
|
|
336
|
+
spec.type = "ClusterIP"
|
|
337
|
+
spec.ports = [svc_http, svc_https]
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
puts "9. Composed resources:"
|
|
341
|
+
puts " Deployment: #{deployment.metadata[:name]} (#{deployment.spec[:replicas]} replicas)"
|
|
342
|
+
puts " - containers: #{deployment.to_h[:spec][:template][:spec][:containers].map { |c| c[:name] }.join(", ")}"
|
|
343
|
+
puts " - volumes: #{deployment.to_h[:spec][:template][:spec][:volumes].map { |v| v[:name] }.join(", ")}"
|
|
344
|
+
puts " - valid? #{deployment.valid?}"
|
|
345
|
+
puts
|
|
346
|
+
puts " Service: #{service.metadata[:name]}"
|
|
347
|
+
puts " - ports: #{service.to_h[:spec][:ports].map { |p| "#{p[:name]}:#{p[:port]}" }.join(", ")}"
|
|
348
|
+
puts " - valid? #{service.valid?}"
|
|
349
|
+
puts
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ══════════════════════════════════════════════════════════════
|
|
353
|
+
# 10. VALIDATION — catching errors on sub-specs
|
|
354
|
+
# ══════════════════════════════════════════════════════════════
|
|
355
|
+
|
|
356
|
+
puts "10. Validation examples:"
|
|
357
|
+
|
|
358
|
+
# Missing required field: Container requires "name"
|
|
359
|
+
bad_container = SubSpec["Container"].new(image: "nginx")
|
|
360
|
+
puts " Container without name: valid? #{bad_container.valid?}"
|
|
361
|
+
|
|
362
|
+
begin
|
|
363
|
+
bad_container.valid!
|
|
364
|
+
rescue Kube::ValidationError => e
|
|
365
|
+
puts " Error: #{e.errors.length} error(s)"
|
|
366
|
+
puts " #{e.message.lines.grep(/required/).first&.strip}"
|
|
367
|
+
end
|
|
368
|
+
puts
|
|
369
|
+
|
|
370
|
+
# Wrong type: ports should be an array
|
|
371
|
+
bad_ports = SubSpec["Container"].new(name: "app", ports: "not-an-array")
|
|
372
|
+
puts " Container with string ports: valid? #{bad_ports.valid?}"
|
|
373
|
+
puts
|
|
374
|
+
|
|
375
|
+
# VolumeMount missing required fields
|
|
376
|
+
bad_mount = SubSpec["VolumeMount"].new(name: "data")
|
|
377
|
+
puts " VolumeMount without mountPath: valid? #{bad_mount.valid?}"
|
|
378
|
+
|
|
379
|
+
begin
|
|
380
|
+
bad_mount.valid!
|
|
381
|
+
rescue Kube::ValidationError => e
|
|
382
|
+
puts " #{e.message.lines.grep(/required/).first&.strip}"
|
|
383
|
+
end
|
|
384
|
+
puts
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ══════════════════════════════════════════════════════════════
|
|
388
|
+
# 11. DISCOVERY — what sub-specs are available?
|
|
389
|
+
# ══════════════════════════════════════════════════════════════
|
|
390
|
+
|
|
391
|
+
instance = Kube::Schema::Instance.new("1.34")
|
|
392
|
+
all_defs = instance.list_definitions
|
|
393
|
+
|
|
394
|
+
puts "11. Discovery:"
|
|
395
|
+
puts " Total definitions available: #{all_defs.length}"
|
|
396
|
+
puts " Container-related:"
|
|
397
|
+
all_defs.select { |d| d.include?("Container") }.each { |d| puts " - #{d}" }
|
|
398
|
+
puts " Volume-related:"
|
|
399
|
+
all_defs.select { |d| d.start_with?("Volume") }.each { |d| puts " - #{d}" }
|
|
400
|
+
puts
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# ══════════════════════════════════════════════════════════════
|
|
404
|
+
# 12. YAML OUTPUT
|
|
405
|
+
# ══════════════════════════════════════════════════════════════
|
|
406
|
+
|
|
407
|
+
manifest = Manifest.new(deployment, service)
|
|
408
|
+
|
|
409
|
+
puts "=" * 60
|
|
410
|
+
puts "YAML Output:"
|
|
411
|
+
puts "=" * 60
|
|
412
|
+
puts manifest.to_yaml
|
data/lib/kube/schema/instance.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Kube
|
|
|
27
27
|
|
|
28
28
|
def initialize(version)
|
|
29
29
|
@resource_classes = {}
|
|
30
|
+
@sub_spec_classes = {}
|
|
30
31
|
|
|
31
32
|
unless Gem::Version.correct?(version)
|
|
32
33
|
raise UnknownVersionError,
|
|
@@ -71,6 +72,39 @@ module Kube
|
|
|
71
72
|
(gvk_index.keys + Schema.custom_schemas.keys).uniq.sort
|
|
72
73
|
end
|
|
73
74
|
|
|
75
|
+
# Look up a sub-spec definition by short name (e.g. "Container",
|
|
76
|
+
# "ContainerPort", "Volume", "Probe"). Returns a class that
|
|
77
|
+
# inherits from Kube::Schema::SubSpec.
|
|
78
|
+
#
|
|
79
|
+
# Also accepts a full definition key like
|
|
80
|
+
# "io.k8s.api.core.v1.Container" for disambiguation.
|
|
81
|
+
#
|
|
82
|
+
# instance.sub_spec("Container") # => SubSpec subclass
|
|
83
|
+
# instance.sub_spec("ContainerPort") # => SubSpec subclass
|
|
84
|
+
#
|
|
85
|
+
def sub_spec(name)
|
|
86
|
+
@sub_spec_classes[name] ||= begin
|
|
87
|
+
definition_key = find_definition_key(name)
|
|
88
|
+
|
|
89
|
+
if definition_key.nil?
|
|
90
|
+
raise "No definition found for #{name}!" \
|
|
91
|
+
"\nUse #list_definitions to see available definitions for v#{version}."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
ref_schema = schemer.ref("#/definitions/#{definition_key}")
|
|
95
|
+
build_sub_spec_class(ref_schema, name)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# All available definition short names for this version.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<String>] sorted, deduplicated short names
|
|
102
|
+
def list_definitions
|
|
103
|
+
schemer.value.fetch("definitions", {}).keys
|
|
104
|
+
.map { |k| k.split(".").last }
|
|
105
|
+
.uniq.sort
|
|
106
|
+
end
|
|
107
|
+
|
|
74
108
|
private
|
|
75
109
|
|
|
76
110
|
# The JSONSchemer instance for this version's Swagger document.
|
|
@@ -214,6 +248,62 @@ module Kube
|
|
|
214
248
|
end
|
|
215
249
|
end
|
|
216
250
|
|
|
251
|
+
# Resolve a short name like "Container" to its full definition key
|
|
252
|
+
# "io.k8s.api.core.v1.Container".
|
|
253
|
+
#
|
|
254
|
+
# Strategy:
|
|
255
|
+
# 1. Exact match on full key (for power users)
|
|
256
|
+
# 2. Short-name match on last segment
|
|
257
|
+
# 3. Prefer stable API versions (v1 > v1beta1 > v1alpha1)
|
|
258
|
+
def find_definition_key(name)
|
|
259
|
+
definitions = schemer.value.fetch("definitions", {})
|
|
260
|
+
|
|
261
|
+
# Exact full-key match
|
|
262
|
+
return name if definitions.key?(name)
|
|
263
|
+
|
|
264
|
+
# Short-name matches (last segment after final ".")
|
|
265
|
+
candidates = definitions.keys.select { |k| k.split(".").last == name }
|
|
266
|
+
return nil if candidates.empty?
|
|
267
|
+
return candidates.first if candidates.size == 1
|
|
268
|
+
|
|
269
|
+
# Disambiguation: prefer stable versions, then beta, then alpha
|
|
270
|
+
candidates.min_by { |k|
|
|
271
|
+
version_segment = k.split(".")[-2].to_s
|
|
272
|
+
case version_segment
|
|
273
|
+
when /\Av\d+\z/ then [0, version_segment]
|
|
274
|
+
when /beta/ then [1, version_segment]
|
|
275
|
+
when /alpha/ then [2, version_segment]
|
|
276
|
+
else [3, version_segment]
|
|
277
|
+
end
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Build a SubSpec subclass from a JSONSchemer instance and a
|
|
282
|
+
# human-readable definition name (for error messages).
|
|
283
|
+
def build_sub_spec_class(schema_instance, definition_name)
|
|
284
|
+
Class.new(::Kube::Schema::SubSpec) do
|
|
285
|
+
@schema = schema_instance
|
|
286
|
+
@definition_name = definition_name
|
|
287
|
+
@schema_properties = @schema.value.fetch("properties", {}).keys.map(&:to_sym)
|
|
288
|
+
|
|
289
|
+
def self.schema
|
|
290
|
+
@schema || superclass.schema
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def self.definition_name
|
|
294
|
+
@definition_name
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.schema_properties
|
|
298
|
+
@schema_properties
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
schema_instance.value.fetch("properties", {}).keys.each do |prop|
|
|
302
|
+
define_method(prop.to_sym) { @data[prop.to_sym] }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
217
307
|
# Called by Kube::Schema.register and reset_custom_schemas! to
|
|
218
308
|
# invalidate cached resource classes so new registrations take effect.
|
|
219
309
|
def clear_resource_cache!
|
|
@@ -345,6 +435,22 @@ if __FILE__ == $0
|
|
|
345
435
|
expect(resource.to_h[:kind]).to eq("VirtualMachine")
|
|
346
436
|
expect(resource.to_h[:metadata][:name]).to eq("test-vm")
|
|
347
437
|
end
|
|
438
|
+
|
|
439
|
+
it "validates a VirtualMachine without $ref resolution errors" do
|
|
440
|
+
resource = instance["VirtualMachine"].new {
|
|
441
|
+
metadata.name = "test-vm"
|
|
442
|
+
spec.runStrategy = "Always"
|
|
443
|
+
spec.template.metadata.labels = { "kubevirt.io/vm": "test-vm" }
|
|
444
|
+
spec.template.spec.domain.cpu = { cores: 1 }
|
|
445
|
+
spec.template.spec.domain.devices = {}
|
|
446
|
+
spec.template.spec.domain.memory = { guest: "1Gi" }
|
|
447
|
+
spec.template.spec.domain.resources = { requests: { memory: "1Gi" } }
|
|
448
|
+
spec.template.spec.volumes = [
|
|
449
|
+
{ name: "rootdisk", containerDisk: { image: "registry.example.com/vm:latest" } }
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
expect { resource.to_yaml }.not_to raise_error
|
|
453
|
+
end
|
|
348
454
|
end
|
|
349
455
|
|
|
350
456
|
describe "class-level schemer cache" do
|