kube_schema 1.2.0 → 1.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7ed4bf90e31bdf3629732faf97da315bbd17b091a622eed5ecb7885a6d793f2
4
- data.tar.gz: 0b8ef4a4ddc76fa357ede482571db2725393d8725fca4a9523568b2156c8f8d0
3
+ metadata.gz: 43157c3d38782006f39aa63c3cc0ae83eb4af0ad3f3974dde30d4ce27aa58a0d
4
+ data.tar.gz: 510e764c90294d6c351c66930b95a056cbdbd7ac0a6f2d88bed0bb7ef374cc76
5
5
  SHA512:
6
- metadata.gz: 788d5c2670f9f1613947444eea6105c5519a3d5ed0ba38bf6bc3c58cda73cea2dce6c34dec16c60ce7e30be301eac3f856e6d4a991e92c30ffe82c593909a5d0
7
- data.tar.gz: 6d3d78dd248b2763cb849af42064baa3ec54ae098b34985a321e21756070de2d62398b6625495af146a2bb977d898e4e174fea81654d71ed41458c68982544c0
6
+ metadata.gz: d38e9e7e866071935b86fd788f8f52f0c751e5b2e71341f3509999cf0beefb193cfa04c92a03f2434865c6bb7771c8348f816b5bc25fd7506c7eb00759fccba3
7
+ data.tar.gz: 6bdab5c5a67196f998e0c8e527964c9921f8cdfc931742fe9a924cf82b4e31c5a54236c22a4bd5c8fe261310affc2cba813a56f248ab38ed6903e5e5bd300056
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_schema (1.1.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.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
- # Kube::Schema
1
+ # kube_schema
2
2
 
3
- Ruby objects for Kubernetes OpenAPI schemas.
3
+ Ruby objects for every Kubernetes resource. Validated against the real OpenAPI spec.
4
4
 
5
- ## Installation
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
- gem "kube_schema"
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
- ## Usage
51
+ Specific versions:
12
52
 
13
53
  ```ruby
14
- Deployment = Kube::Schema["1.33.6"]["Deployment"]
54
+ Kube::Schema["1.34"]["Deployment"]
55
+ Kube::Schema["1.31"]["Pod"]
56
+ ```
15
57
 
16
- app = Deployment.new do
17
- self.description = "My app"
18
- self.type = "custom"
19
- end
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
- app.description # => "My app"
22
- app.type # => "custom"
23
- app.properties # => {"apiVersion" => ..., "kind" => ..., ...}
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["1.33.6"]["Deployment"]
28
- def is_the_best?
29
- true
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
- app.is_the_best? # => true
35
- app.description # => "Deployment enables declarative updates for Pods and ReplicaSets."
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
+ ![Validation error output](assets/validation-error.png)
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/kube_schema.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  Abstractions are God. Let's give praise and build on the shoulders of giants by using objects to represent schemas. It was written and to it became.
15
15
  DESC
16
16
 
17
- spec.homepage = "https://github.com/n-at-han-k/kube_schema"
17
+ spec.homepage = "https://github.com/general-intelligence-systems/kube_schema"
18
18
  spec.license = "Apache-2.0"
19
19
  spec.required_ruby_version = ">= 3.2.0"
20
20
 
@@ -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
- hash = deep_stringify_keys(hash)
10
+ deep_symbolize_keys(self.class.defaults.to_h).then do |defaults|
14
11
 
15
- @data = BlackHoleStruct.new(hash)
16
- @data.instance_exec(&block) if block
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
- return true if self.class.schema.nil?
32
-
33
- self.class.schema.valid?(deep_stringify_keys(to_h))
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
- return true if self.class.schema.nil?
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
- data = deep_stringify_keys(to_h)
42
- errors = self.class.schema.validate(data).to_a
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
- unless errors.empty?
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
- deep_stringify_keys(to_h).to_yaml
86
+ if valid!
87
+ deep_stringify_keys(to_h).to_yaml
88
+ end
75
89
  end
76
90
 
77
91
  def ==(other)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Schema
5
- VERSION = "1.2.0"
5
+ VERSION = "1.2.2"
6
6
  end
7
7
  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.0
4
+ version: 1.2.2
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
@@ -144,13 +145,13 @@ files:
144
145
  - schemas/v1.33.json
145
146
  - schemas/v1.34.json
146
147
  - schemas/v1.35.json
147
- homepage: https://github.com/n-at-han-k/kube_schema
148
+ homepage: https://github.com/general-intelligence-systems/kube_schema
148
149
  licenses:
149
150
  - Apache-2.0
150
151
  metadata:
151
- homepage_uri: https://github.com/n-at-han-k/kube_schema
152
- source_code_uri: https://github.com/n-at-han-k/kube_schema
153
- documentation_uri: https://github.com/n-at-han-k/kube_schema
152
+ homepage_uri: https://github.com/general-intelligence-systems/kube_schema
153
+ source_code_uri: https://github.com/general-intelligence-systems/kube_schema
154
+ documentation_uri: https://github.com/general-intelligence-systems/kube_schema
154
155
  rubygems_mfa_required: 'true'
155
156
  rdoc_options: []
156
157
  require_paths: