kube_schema 1.2.1 → 1.2.3
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 +2 -2
- data/README.md +242 -19
- data/assets/validation-error.png +0 -0
- data/lib/kube/schema/resource.rb +40 -26
- data/lib/kube/schema/version.rb +1 -1
- data/lib/kube/schema.rb +48 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d4d0fd333c3834e4593b60c9c57ada0fa0fd275175f55ffcfcc0616cd64fef1d
|
|
4
|
+
data.tar.gz: c427e577b4d92bc9241c21da35ae7f863f9ac0c1e354696a31e5292c8f818089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 752d0e5bae651ce4734e46ac9a886f06253dacd5193861729509c92e39bdfca81a543ad3267440b1499725147466783e1aca904312a1184e9c6ce77d311d1282
|
|
7
|
+
data.tar.gz: 893b866464d3f4f8565d7e73c840c1f2cfaf4421dcfeeb867374a606d1b7b34b89118a4ccecbba47f58c767f33c4d2969deaa427b4ad3e38117f987c8af76db7
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kube_schema (1.
|
|
4
|
+
kube_schema (1.2.1)
|
|
5
5
|
black_hole_struct (~> 0.1)
|
|
6
6
|
json_schemer (~> 2.5)
|
|
7
7
|
rubyshell (~> 1.5)
|
|
@@ -10,7 +10,7 @@ GEM
|
|
|
10
10
|
remote: https://rubygems.org/
|
|
11
11
|
specs:
|
|
12
12
|
ast (2.4.3)
|
|
13
|
-
bigdecimal (4.1.
|
|
13
|
+
bigdecimal (4.1.2)
|
|
14
14
|
black_hole_struct (0.1.3)
|
|
15
15
|
diff-lcs (1.6.2)
|
|
16
16
|
hana (1.3.7)
|
data/README.md
CHANGED
|
@@ -1,36 +1,259 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kube_schema
|
|
2
2
|
|
|
3
|
-
Ruby objects for Kubernetes OpenAPI
|
|
3
|
+
Ruby objects for every Kubernetes resource. Validated against the real OpenAPI spec.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```ruby
|
|
6
|
+
Kube::Schema["Deployment"].new {
|
|
7
|
+
metadata.name = "web"
|
|
8
|
+
metadata.namespace = "prod"
|
|
9
|
+
spec.replicas = 3
|
|
10
|
+
spec.template.spec.containers = [
|
|
11
|
+
{ name: "app", image: "nginx:1.27", ports: [{ containerPort: 80 }] }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
No YAML. No hash literals. Just Ruby blocks that know their schema.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Resources](#resources)
|
|
22
|
+
- [The block DSL](#the-block-dsl)
|
|
23
|
+
- [Subclassing](#subclassing)
|
|
24
|
+
- [Validation](#validation)
|
|
25
|
+
- [Error messages](#error-messages)
|
|
26
|
+
- [Manifests](#manifests)
|
|
27
|
+
- [File I/O](#file-io)
|
|
28
|
+
- [Composition](#composition)
|
|
29
|
+
- [Enumerable](#enumerable)
|
|
30
|
+
- [Schema versions](#schema-versions)
|
|
31
|
+
- [Related projects](#related-projects)
|
|
32
|
+
- [Built with](#built-with)
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
gem install kube_schema
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Resources
|
|
41
|
+
|
|
42
|
+
Every Kubernetes kind is a class. Fetch it by name.
|
|
6
43
|
|
|
7
44
|
```ruby
|
|
8
|
-
|
|
45
|
+
Kube::Schema["Deployment"] # => Class < Kube::Schema::Resource
|
|
46
|
+
Kube::Schema["Service"]
|
|
47
|
+
Kube::Schema["ConfigMap"]
|
|
48
|
+
Kube::Schema["NetworkPolicy"]
|
|
9
49
|
```
|
|
10
50
|
|
|
11
|
-
|
|
51
|
+
Specific versions:
|
|
12
52
|
|
|
13
53
|
```ruby
|
|
14
|
-
|
|
54
|
+
Kube::Schema["1.34"]["Deployment"]
|
|
55
|
+
Kube::Schema["1.31"]["Pod"]
|
|
56
|
+
```
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
58
|
+
Discovery:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
Kube::Schema.schema_versions # => ["1.19", "1.20", ..., "1.35"]
|
|
62
|
+
Kube::Schema.latest_version # => "1.35"
|
|
63
|
+
|
|
64
|
+
Kube::Schema["1.34"].list_resources
|
|
65
|
+
# => ["Binding", "CSIDriver", "ConfigMap", "Deployment", ...]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## The block DSL
|
|
69
|
+
|
|
70
|
+
Nested attributes just work. No intermediate hashes, no string keys.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
deploy = Kube::Schema["Deployment"].new {
|
|
74
|
+
metadata.name = "web"
|
|
75
|
+
metadata.namespace = "prod"
|
|
76
|
+
metadata.labels = { app: "web" }
|
|
77
|
+
|
|
78
|
+
spec.replicas = 3
|
|
79
|
+
spec.selector.matchLabels = { app: "web" }
|
|
80
|
+
|
|
81
|
+
spec.template.metadata.labels = { app: "web" }
|
|
82
|
+
spec.template.spec.containers = [
|
|
83
|
+
{ name: "app", image: "nginx:1.27", ports: [{ containerPort: 80 }] }
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`apiVersion` and `kind` are derived from the schema automatically:
|
|
20
89
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
90
|
+
```ruby
|
|
91
|
+
deploy.to_h[:apiVersion] # => "apps/v1"
|
|
92
|
+
deploy.to_h[:kind] # => "Deployment"
|
|
24
93
|
```
|
|
25
94
|
|
|
95
|
+
## Subclassing
|
|
96
|
+
|
|
26
97
|
```ruby
|
|
27
|
-
class RailsApp < Kube::Schema["
|
|
28
|
-
def
|
|
29
|
-
|
|
98
|
+
class RailsApp < Kube::Schema["Deployment"]
|
|
99
|
+
def default_image
|
|
100
|
+
"ruby:3.4"
|
|
30
101
|
end
|
|
31
102
|
end
|
|
32
103
|
|
|
33
|
-
app = RailsApp.new
|
|
34
|
-
|
|
35
|
-
|
|
104
|
+
app = RailsApp.new {
|
|
105
|
+
metadata.name = "rails"
|
|
106
|
+
spec.template.spec.containers = [
|
|
107
|
+
{ name: "web", image: "myapp:latest" }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
app.default_image # => "ruby:3.4"
|
|
112
|
+
app.valid? # => true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Validation
|
|
116
|
+
|
|
117
|
+
Every resource validates against the full Kubernetes OpenAPI spec.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
deploy.valid? # => true
|
|
121
|
+
|
|
122
|
+
bad = Kube::Schema["Deployment"].new {
|
|
123
|
+
self.apiVersion = 12345
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
bad.valid? # => false
|
|
127
|
+
|
|
128
|
+
bad.valid!
|
|
129
|
+
# Kube::ValidationError: Schema validation failed for Deployment:
|
|
130
|
+
# - apiVersion = 12345 — expected string, got Integer
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`to_yaml` refuses to serialize invalid resources:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
bad.to_yaml # raises Kube::ValidationError
|
|
36
137
|
```
|
|
138
|
+
|
|
139
|
+
## Error messages
|
|
140
|
+
|
|
141
|
+
Validation errors render annotated YAML with color-coded diagnostics. Error lines are highlighted in red, missing required keys are injected inline, and each problem gets a clear explanation:
|
|
142
|
+
|
|
143
|
+

|
|
144
|
+
|
|
145
|
+
## Manifests
|
|
146
|
+
|
|
147
|
+
Group resources into multi-document YAML.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
manifest = Kube::Schema::Manifest.new
|
|
151
|
+
|
|
152
|
+
manifest << Kube::Schema["Namespace"].new {
|
|
153
|
+
metadata.name = "prod"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
manifest << Kube::Schema["Deployment"].new {
|
|
157
|
+
metadata.name = "web"
|
|
158
|
+
metadata.namespace = "prod"
|
|
159
|
+
spec.replicas = 3
|
|
160
|
+
spec.template.spec.containers = [
|
|
161
|
+
{ name: "app", image: "nginx:1.27" }
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
manifest << Kube::Schema["Service"].new {
|
|
166
|
+
metadata.name = "web"
|
|
167
|
+
metadata.namespace = "prod"
|
|
168
|
+
spec.selector = { app: "web" }
|
|
169
|
+
spec.ports = [{ port: 80, targetPort: 8080 }]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
puts manifest.to_yaml
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
---
|
|
177
|
+
apiVersion: v1
|
|
178
|
+
kind: Namespace
|
|
179
|
+
metadata:
|
|
180
|
+
name: prod
|
|
181
|
+
---
|
|
182
|
+
apiVersion: apps/v1
|
|
183
|
+
kind: Deployment
|
|
184
|
+
metadata:
|
|
185
|
+
name: web
|
|
186
|
+
namespace: prod
|
|
187
|
+
spec:
|
|
188
|
+
replicas: 3
|
|
189
|
+
...
|
|
190
|
+
---
|
|
191
|
+
apiVersion: v1
|
|
192
|
+
kind: Service
|
|
193
|
+
metadata:
|
|
194
|
+
name: web
|
|
195
|
+
namespace: prod
|
|
196
|
+
spec:
|
|
197
|
+
selector:
|
|
198
|
+
app: web
|
|
199
|
+
ports:
|
|
200
|
+
- port: 80
|
|
201
|
+
targetPort: 8080
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### File I/O
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# Write
|
|
208
|
+
manifest.write("cluster.yaml")
|
|
209
|
+
|
|
210
|
+
# Read
|
|
211
|
+
loaded = Kube::Schema::Manifest.open("cluster.yaml")
|
|
212
|
+
|
|
213
|
+
# Modify and write back
|
|
214
|
+
loaded << new_resource
|
|
215
|
+
loaded.write
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Composition
|
|
219
|
+
|
|
220
|
+
Manifests flatten into each other. No nesting.
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
infra = Kube::Schema::Manifest.new(namespace, configmap, secret)
|
|
224
|
+
app = Kube::Schema::Manifest.new(deployment, service)
|
|
225
|
+
|
|
226
|
+
combined = Kube::Schema::Manifest.new
|
|
227
|
+
combined << infra
|
|
228
|
+
combined << app
|
|
229
|
+
combined.size # => 5
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Enumerable
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
manifest.map { |r| r.to_h[:kind] }
|
|
236
|
+
manifest.select { |r| r.to_h[:kind] == "Service" }
|
|
237
|
+
manifest.any? { |r| r.to_h[:kind] == "Pod" }
|
|
238
|
+
manifest.count
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Schema versions
|
|
242
|
+
|
|
243
|
+
Bundled schemas ship with the gem for Kubernetes 1.19 through 1.35. Updated automatically via CI.
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Kube::Schema.schema_version = "1.31"
|
|
247
|
+
Kube::Schema["Deployment"] # uses 1.31
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Related projects
|
|
251
|
+
|
|
252
|
+
- [kube_cluster](https://github.com/general-intelligence-systems/kube_cluster) -- OOP resource management with dirty tracking and persistence
|
|
253
|
+
- [kube_kubectl](https://github.com/general-intelligence-systems/kube_ctl) -- Ruby DSL that compiles to kubectl and helm commands
|
|
254
|
+
- [kube_kit](https://github.com/general-intelligence-systems/kube_kit) -- Generators for kube_cluster projects
|
|
255
|
+
- [kube_engine](https://github.com/general-intelligence-systems/kube_engine) -- Kubernetes engine
|
|
256
|
+
|
|
257
|
+
## Built with
|
|
258
|
+
|
|
259
|
+
[black_hole_struct](https://github.com/aanand/black_hole_struct) -- the dynamic nested struct that powers the block DSL. [json_schemer](https://github.com/davishmcclurg/json_schemer) -- JSON Schema validation against the real Kubernetes OpenAPI specs.
|
|
Binary file
|
data/lib/kube/schema/resource.rb
CHANGED
|
@@ -6,48 +6,61 @@ module Kube
|
|
|
6
6
|
module Schema
|
|
7
7
|
class Resource
|
|
8
8
|
|
|
9
|
-
# Subclasses generated by Instance#[] have a class-level .schema
|
|
10
|
-
# which is a JSONSchemer::Schema object, and .defaults which
|
|
11
|
-
# provides apiVersion and kind derived from the GVK metadata.
|
|
12
9
|
def initialize(hash = {}, &block)
|
|
13
|
-
|
|
10
|
+
deep_symbolize_keys(self.class.defaults.to_h).then do |defaults|
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
deep_symbolize_keys(hash).then do |symbolized|
|
|
13
|
+
config = defaults.merge({
|
|
14
|
+
metadata: symbolized.delete(:metadata),
|
|
15
|
+
spec: symbolized.delete(:spec),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
@data = BlackHoleStruct.new(config)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if block_given?
|
|
23
|
+
@data.instance_exec(&block)
|
|
24
|
+
end
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
def apiVersion = @data.apiVersion
|
|
28
|
+
def kind = @data.kind
|
|
29
|
+
def spec = @data.spec
|
|
30
|
+
def metadata = @data.metadata
|
|
31
|
+
|
|
19
32
|
# Gets overridden by the factory in Kube::Schema::Instance
|
|
20
|
-
def self.schema
|
|
21
|
-
nil
|
|
22
|
-
end
|
|
33
|
+
def self.schema = nil
|
|
23
34
|
|
|
24
35
|
# Gets overridden by the factory in Kube::Schema::Instance.
|
|
25
36
|
# Returns a frozen Hash like { "apiVersion" => "apps/v1", "kind" => "Deployment" }
|
|
26
|
-
def self.defaults
|
|
27
|
-
nil
|
|
28
|
-
end
|
|
37
|
+
def self.defaults = nil
|
|
29
38
|
|
|
30
39
|
def valid?
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
if self.class.schema.nil?
|
|
41
|
+
true
|
|
42
|
+
else
|
|
43
|
+
self.class.schema.valid?(deep_stringify_keys(to_h))
|
|
44
|
+
end
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
# Like #valid? but raises Kube::ValidationError with details on failure.
|
|
37
48
|
# The error message includes the resource kind and name for context.
|
|
38
49
|
def valid!
|
|
39
|
-
|
|
50
|
+
if self.class.schema.nil?
|
|
51
|
+
true
|
|
52
|
+
else
|
|
53
|
+
data = deep_stringify_keys(to_h)
|
|
54
|
+
errors = self.class.schema.validate(data).to_a
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
unless errors.empty?
|
|
57
|
+
kind = self.class.defaults&.dig("kind")
|
|
58
|
+
name = data.dig("metadata", "name")
|
|
59
|
+
raise Kube::ValidationError.new(errors, kind: kind, name: name, manifest: data)
|
|
60
|
+
end
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
kind = self.class.defaults&.dig("kind")
|
|
46
|
-
name = data.dig("metadata", "name")
|
|
47
|
-
raise Kube::ValidationError.new(errors, kind: kind, name: name, manifest: data)
|
|
62
|
+
true
|
|
48
63
|
end
|
|
49
|
-
|
|
50
|
-
true
|
|
51
64
|
end
|
|
52
65
|
|
|
53
66
|
# Returns the resource data as a Hash. Defaults (apiVersion, kind)
|
|
@@ -70,8 +83,9 @@ module Kube
|
|
|
70
83
|
# Serializes to clean Kubernetes YAML.
|
|
71
84
|
# Raises Kube::ValidationError if the resource is not valid.
|
|
72
85
|
def to_yaml
|
|
73
|
-
valid!
|
|
74
|
-
|
|
86
|
+
if valid!
|
|
87
|
+
deep_stringify_keys(to_h).to_yaml
|
|
88
|
+
end
|
|
75
89
|
end
|
|
76
90
|
|
|
77
91
|
def ==(other)
|
data/lib/kube/schema/version.rb
CHANGED
data/lib/kube/schema.rb
CHANGED
|
@@ -75,3 +75,51 @@ module Kube
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
|
+
|
|
79
|
+
# Patch BlackHoleStruct to handle arrays consistently.
|
|
80
|
+
#
|
|
81
|
+
# The upstream gem does not recurse into arrays — hashes inside arrays
|
|
82
|
+
# are not converted to BlackHoleStruct on construction, and are not
|
|
83
|
+
# converted back to plain Hash on #to_h. This causes key-type
|
|
84
|
+
# inconsistencies after a Resource round-trip (symbol keys become
|
|
85
|
+
# string keys inside arrays).
|
|
86
|
+
#
|
|
87
|
+
# These two patches fix both directions:
|
|
88
|
+
# initialize — converts hashes inside arrays to BlackHoleStruct
|
|
89
|
+
# to_h — converts BlackHoleStruct/arrays back to plain objects
|
|
90
|
+
class BlackHoleStruct
|
|
91
|
+
def initialize(hash = {})
|
|
92
|
+
raise ArgumentError, "Argument should be a Hash" unless hash.is_a?(Hash)
|
|
93
|
+
|
|
94
|
+
@table = {}
|
|
95
|
+
hash.each do |key, value|
|
|
96
|
+
@table[key.to_sym] = deep_wrap(value)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_h
|
|
101
|
+
hash = {}
|
|
102
|
+
@table.each do |key, value|
|
|
103
|
+
hash[key] = deep_unwrap(value)
|
|
104
|
+
end
|
|
105
|
+
hash
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def deep_wrap(value)
|
|
111
|
+
case value
|
|
112
|
+
when Hash then self.class.new(value)
|
|
113
|
+
when Array then value.map { |v| deep_wrap(v) }
|
|
114
|
+
else value
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def deep_unwrap(value)
|
|
119
|
+
case value
|
|
120
|
+
when self.class then value.to_h
|
|
121
|
+
when Array then value.map { |v| deep_unwrap(v) }
|
|
122
|
+
else value
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kube_schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan K
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- Gemfile.lock
|
|
116
116
|
- README.md
|
|
117
117
|
- Rakefile
|
|
118
|
+
- assets/validation-error.png
|
|
118
119
|
- bin/console
|
|
119
120
|
- bin/copy-schemas-over
|
|
120
121
|
- bin/increment-version
|