kube_schema 1.3.0 → 1.3.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.
@@ -39,44 +39,47 @@ module Kube
39
39
 
40
40
  # Look up a resource by kind (e.g. "Deployment", "NetworkPolicy").
41
41
  # Returns a class that inherits from Kube::Schema::Resource.
42
+ #
43
+ # Custom schemas registered via Kube::Schema.register take precedence
44
+ # over built-in definitions, allowing users to override or extend the
45
+ # schema for any kind.
42
46
  def [](kind)
43
47
  @resource_classes[kind] ||= begin
44
- entry = find_gvk_entry(kind)
45
-
46
- if entry.nil?
47
- raise "No resource schema found for #{kind}!" \
48
- "\nUse #list_resources to see available kinds for v#{version}."
49
- end
50
-
51
- ref_schema = schemer.ref("#/definitions/#{entry[:definition_key]}")
52
- defaults = entry[:defaults].freeze
53
-
54
- Class.new(::Kube::Schema::Resource) do
55
- @schema = ref_schema
56
- @defaults = defaults
57
-
58
- def self.schema
59
- @schema || superclass.schema
48
+ # Custom schemas win over built-in definitions.
49
+ custom = find_custom_entry(kind)
50
+ if custom
51
+ build_resource_class(custom[:schema], custom[:defaults])
52
+ else
53
+ entry = find_gvk_entry(kind)
54
+
55
+ if entry.nil?
56
+ raise "No resource schema found for #{kind}!" \
57
+ "\nUse #list_resources to see available kinds for v#{version}."
60
58
  end
61
59
 
62
- def self.defaults
63
- @defaults || superclass.defaults
64
- end
60
+ ref_schema = schemer.ref("#/definitions/#{entry[:definition_key]}")
61
+ build_resource_class(ref_schema, entry[:defaults].freeze)
65
62
  end
66
63
  end
67
64
  end
68
65
 
69
- # All available resource kinds for this version.
66
+ # All available resource kinds for this version, including any
67
+ # custom schemas registered via Kube::Schema.register.
70
68
  #
71
69
  # @return [Array<String>] sorted kind names
72
70
  def list_resources
73
- gvk_index.keys.sort
71
+ (gvk_index.keys + Schema.custom_schemas.keys).uniq.sort
74
72
  end
75
73
 
76
74
  private
77
75
 
78
76
  # The JSONSchemer instance for this version's Swagger document.
79
77
  # Cached at the class level so it's built once per version.
78
+ #
79
+ # After loading the base Swagger JSON, merges in any extra definition
80
+ # files found in the schemas directory (e.g. crd-definitions.json,
81
+ # loft-definitions.json). These files are flat JSON objects where keys
82
+ # are definition names and values are OpenAPI v2 schema objects.
80
83
  def schemer
81
84
  self.class.schemers[@version] ||= begin
82
85
  path = File.join(SCHEMAS_DIR, "v#{version}.json")
@@ -87,7 +90,18 @@ module Kube
87
90
  "\nUse `Kube::Schema.schema_versions` to get a list."
88
91
  end
89
92
 
90
- JSONSchemer.schema(JSON.parse(File.read(path)))
93
+ schema = JSON.parse(File.read(path))
94
+
95
+ # Merge extra definition files (*-definitions.json) into the
96
+ # base schema so CRD and aggregated-API types (e.g. loft,
97
+ # gateway-api) are available alongside built-in k8s types.
98
+ Dir.glob(File.join(SCHEMAS_DIR, "*-definitions.json")).each do |defs_path|
99
+ extra = JSON.parse(File.read(defs_path))
100
+ schema["definitions"] ||= {}
101
+ schema["definitions"].merge!(extra)
102
+ end
103
+
104
+ JSONSchemer.schema(schema)
91
105
  end
92
106
  end
93
107
 
@@ -151,6 +165,41 @@ module Kube
151
165
 
152
166
  nil
153
167
  end
168
+
169
+ # Find a custom schema entry by kind (case-insensitive).
170
+ # Returns the { schema:, defaults: } hash or nil.
171
+ def find_custom_entry(kind)
172
+ registry = Schema.custom_schemas
173
+ return registry[kind] if registry.key?(kind)
174
+
175
+ registry.each do |k, v|
176
+ return v if k.downcase == kind.downcase
177
+ end
178
+
179
+ nil
180
+ end
181
+
182
+ # Build a Resource subclass from a JSONSchemer instance and defaults hash.
183
+ def build_resource_class(schema_instance, defaults)
184
+ Class.new(::Kube::Schema::Resource) do
185
+ @schema = schema_instance
186
+ @defaults = defaults
187
+
188
+ def self.schema
189
+ @schema || superclass.schema
190
+ end
191
+
192
+ def self.defaults
193
+ @defaults || superclass.defaults
194
+ end
195
+ end
196
+ end
197
+
198
+ # Called by Kube::Schema.register and reset_custom_schemas! to
199
+ # invalidate cached resource classes so new registrations take effect.
200
+ def clear_resource_cache!
201
+ @resource_classes.clear
202
+ end
154
203
  end
155
204
  end
156
205
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "black_hole_struct"
4
-
5
3
  module Kube
6
4
  module Schema
7
5
  class Resource
@@ -9,13 +7,16 @@ module Kube
9
7
  def initialize(hash = {}, &block)
10
8
  deep_symbolize_keys(self.class.defaults.to_h).then do |defaults|
11
9
 
10
+ # You are NEVER allowed to change `apiVersion` or `kind`
11
+ # Therefore, they are ONLY ever set from the self.defaults
12
+ # property.
12
13
  deep_symbolize_keys(hash).then do |symbolized|
13
14
  config = defaults.merge({
14
- metadata: symbolized.delete(:metadata),
15
- spec: symbolized.delete(:spec),
15
+ metadata: symbolized.delete(:metadata) || {},
16
+ spec: symbolized.delete(:spec) || {},
16
17
  })
17
18
 
18
- @data = BlackHoleStruct.new(config)
19
+ @data = config
19
20
  end
20
21
  end
21
22
 
@@ -68,7 +69,7 @@ module Kube
68
69
  # they are facts derived from the GVK metadata.
69
70
  def to_h
70
71
  defaults = self.class.defaults
71
- data = @data.to_h
72
+ data = @data.reject { |_, v| v.is_a?(Hash) && v.empty? }
72
73
 
73
74
  if defaults
74
75
  symbolized = deep_symbolize_keys(defaults)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Schema
5
- VERSION = "1.3.0"
5
+ VERSION = "1.3.2"
6
6
  end
7
7
  end
data/lib/kube/schema.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../monkey_patches'
4
- require_relative '../kube/errors'
3
+ require_relative 'monkey_patches'
4
+ require_relative 'errors'
5
5
  require_relative 'schema/version'
6
6
  require_relative 'schema/resource'
7
7
  require_relative 'schema/instance'
@@ -16,6 +16,7 @@ module Kube
16
16
 
17
17
  @schema_version = nil
18
18
  @instances = {}
19
+ @custom_schemas = {}
19
20
 
20
21
  GEM_ROOT = File.expand_path("../..", __dir__).freeze
21
22
  SCHEMAS_DIR = File.join(GEM_ROOT, "schemas").freeze
@@ -26,6 +27,71 @@ module Kube
26
27
  # When nil, the DEFAULT_VERSION is used.
27
28
  attr_accessor :schema_version
28
29
 
30
+ # Custom schemas registered via Kube::Schema.register.
31
+ # Keys are kind strings, values are { schema:, defaults: } hashes.
32
+ attr_reader :custom_schemas
33
+
34
+ # Register a standalone JSON Schema for a custom resource kind.
35
+ #
36
+ # This lets users add CRD schemas from any source — for example,
37
+ # the datreeio/CRDs-catalog, operator repos, or their own CRDs.
38
+ # Registered kinds take precedence over built-in definitions.
39
+ #
40
+ # @param kind [String] The Kubernetes Kind (e.g. "Certificate")
41
+ # @param schema [Hash, String, Pathname] JSON Schema as a Hash,
42
+ # a JSON string, or a file path to a .json file
43
+ # @param api_version [String] The apiVersion (e.g. "cert-manager.io/v1")
44
+ #
45
+ # @example Register from a local file
46
+ # Kube::Schema.register("Certificate",
47
+ # schema: "schemas/cert-manager.io/certificate_v1.json",
48
+ # api_version: "cert-manager.io/v1"
49
+ # )
50
+ #
51
+ # @example Register from a Hash
52
+ # Kube::Schema.register("MyResource",
53
+ # schema: { "type" => "object", "properties" => { ... } },
54
+ # api_version: "example.com/v1"
55
+ # )
56
+ #
57
+ def register(kind, schema:, api_version:)
58
+ require "json"
59
+ require "json_schemer"
60
+
61
+ parsed = case schema
62
+ when Hash
63
+ schema
64
+ when String, Pathname
65
+ path = schema.to_s
66
+ if File.exist?(path)
67
+ JSON.parse(File.read(path))
68
+ else
69
+ JSON.parse(path)
70
+ end
71
+ else
72
+ raise ArgumentError,
73
+ "schema must be a Hash, a JSON string, or a file path — got #{schema.class}"
74
+ end
75
+
76
+ @custom_schemas[kind] = {
77
+ schema: JSONSchemer.schema(parsed),
78
+ defaults: { "apiVersion" => api_version, "kind" => kind }.freeze
79
+ }
80
+
81
+ # Invalidate cached resource classes on all instances so the
82
+ # new registration takes effect immediately.
83
+ @instances.each_value { |inst| inst.send(:clear_resource_cache!) }
84
+
85
+ kind
86
+ end
87
+
88
+ # Remove all custom schema registrations.
89
+ # Useful for test teardown or resetting state.
90
+ def reset_custom_schemas!
91
+ @custom_schemas.clear
92
+ @instances.each_value { |inst| inst.send(:clear_resource_cache!) }
93
+ end
94
+
29
95
  # Kube::Schema["1.34"] => cached Instance (supports ["Deployment"] chaining)
30
96
  # Kube::Schema["Deployment"] => Resource via the default version
31
97
  def [](key)
@@ -45,7 +111,9 @@ module Kube
45
111
  )
46
112
  end
47
113
  else
48
- Instance.new(schema_version || DEFAULT_VERSION)[key]
114
+ version = schema_version || DEFAULT_VERSION
115
+ @instances[version] ||= Instance.new(version)
116
+ @instances[version][key]
49
117
  end
50
118
  end
51
119