kubernetes_template_rendering 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5db190341419a622d7593eea77f9ad5525839c0c2039d540edd55a6ce8117479
4
- data.tar.gz: ac8e1ec55caccb59e15cc7b552c58e433f14ac3d3946b9100a1895cfcab97ea2
3
+ metadata.gz: 2834614aaa2465ab9be3a1c50b6c2d2a1b1f4264ecadafddcb5bf8365c5bb151
4
+ data.tar.gz: 6142bd0df1d9da48f1058409017f2080082deb81d13b4c02b4dce2bef2ed80ef
5
5
  SHA512:
6
- metadata.gz: 34090f91339fc87a45fb91bd3174f281953d952f618ee91b035fb4ecc1dca9c9f2f1cbc8c0f7450cdac739c93229e5b18c4a7e547d22bc735f550808747de74b
7
- data.tar.gz: 8bac2935eb16a00898e10f132f8fb5260a71feae470719267111ea2118c3a2d25267130e8d71683936e4faa40473aa8616f2ba90060abdbcf1af29e2a8c721be
6
+ metadata.gz: bd84f186eda857691bb2825188bcabb8dfc8af8e9c9054623263d72db981d3bba853177f31fd63de4a57f6b982ead442724515a54cc2c42b0454c41c0a9aff6f
7
+ data.tar.gz: 8aa7fac766cd4f19e6994a1a51a68bbaf09db56384ae979d403ee4f795a6d7d78a0d9fc4211330d620253033a730306a7eb3d0541995a6436ecfe150a720320e
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
4
 
5
5
  Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0] - 2026-07-23
8
+ ### Added
9
+ - `--variable-override` now supports dotted-path keys (`components.webServer.hpa.minReplicas:2`) that deep-merge into nested variables, with JSON value coercion (integers, floats, booleans, `null`, quoted strings; non-JSON values stay raw strings) and `\.` escaping for literal dots. Plain `KEY:VALUE` (no dot) behaves exactly as before: top-level key, raw string value.
10
+ - Added `--variable-override-json '<json object>'` (repeatable), deep-merged with all other override flags in command-line order (later flags win). Use it for values containing commas or whole structures.
11
+ - Overrides are echoed once to stdout at render start (`Variable overrides (deep-merged after definitions.yaml): {...}`). Rendered files still carry no override comment (see 0.3.0).
12
+
13
+ ### Changed
14
+ - `Template#initialize` merges `variable_overrides` with `deep_merge` instead of `merge`. Behavior-identical for all previously-valid inputs (legacy override values are always strings, so no hash-vs-hash merge could occur).
15
+
16
+ ## [0.6.2] - 2026-07-15
17
+ ### Fixed
18
+ - Fixed the `--reconcile` sweep root for entries whose `subdirectory:` nests more than one level deep (e.g. `subdirectory: exclude-argocd/auth`). The sweep root is now the entry's canonical base — `<region>/<cluster_type>/<color>` for non-SPP entries and `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER` for SPP entries — regardless of `subdirectory:` depth, rather than the immediate parent of the rendered output directory. Previously a nested `subdirectory:` pushed the sweep root one level too deep, so when a `subdirectory:` was renamed or nested deeper the files left at the old shallower path were siblings of the sweep root and were never swept.
19
+
20
+ ### Changed
21
+ - Under `--reconcile`, non-SPP entries must now render within their canonical `<region>/<cluster_type>/<color>` base; an entry that renders outside it (only reachable via the deprecated `directory:` field) hard-errors before any writes. This completes the reconcile layout hard-validation deferred in ADR-0001 — SPP entries were already constrained to the `spp/SPP-PLACEHOLDER` prefix. See ADR-0001.
22
+
7
23
  ## [0.6.1] - 2026-07-08
8
24
  ### Fixed
9
25
  - Fixed `variable_overrides` and `source_repo` not being forwarded to child `Resource` instances created by `DeployGroupedResource`. Previously, `--variable-override` values (e.g. `deploySha`) were silently unavailable inside all `*-deploy.jsonnet` and `*-deploy.yaml.erb` templates, causing a `KeyError` at render time. `DeployGroupedResource#initialize` now accepts both as optional keyword arguments and passes them through to each `Resource.new` call in `#render`. `ResourceSet#grouped_resources` is updated to supply both values.
data/README.md CHANGED
@@ -71,6 +71,43 @@ Useful when one `--cluster_type` matches multiple sibling entries (e.g. `staging
71
71
 
72
72
  If an `--only` value matches no entry across any template directory, the gem raises with the list of valid keys so the caller can self-correct.
73
73
 
74
+ ### Variable overrides
75
+
76
+ Pass `--variable-override KEY:VALUE` (repeatable) to override values from `definitions.yaml` after all per-section variables and auto-injected region/cluster vars are resolved. Overrides always win.
77
+
78
+ **Legacy form (no dot in KEY):** sets a top-level key to the raw string value — exactly the pre-0.7.0 behavior. Values are never type-coerced (`deploySha:12345` stays the string `"12345"`; `enabled:true` stays `"true"`). The value is everything after the first colon (`image:registry:5000/app` → `"registry:5000/app"`). Arguments with no colon are silently ignored.
79
+
80
+ ```bash
81
+ gem exec -g kubernetes_template_rendering render_templates \
82
+ --rendered-directory path/to/resources \
83
+ --variable-override deploySha:abc123 \
84
+ deployment/templates
85
+ ```
86
+
87
+ **Dotted-path form (KEY contains `.`):** the key is split on unescaped dots into a nested path and deep-merged into variables. The value is JSON-coerced when it parses as JSON (`2` → integer, `true`/`false` → booleans, `null` → nil, `"2"` → string); otherwise the raw string is kept (`hello` → `"hello"`, `02` → `"02"`).
88
+
89
+ ```bash
90
+ gem exec -g kubernetes_template_rendering render_templates \
91
+ --rendered-directory path/to/resources \
92
+ --variable-override components.webServer.hpa.minReplicas:2 \
93
+ deployment/templates
94
+ ```
95
+
96
+ Escape literal dots in a segment with `\.` (e.g. `metadata.labels.app\.kubernetes\.io/name:foo`). Only `\.` is an escape sequence; all other backslashes — including trailing ones — are kept literally. Keys that would need a literal backslash immediately before a path-splitting dot must use `--variable-override-json` instead.
97
+
98
+ **`--variable-override-json` (repeatable):** deep-merges a JSON object. Use this for values containing commas or whole arrays/objects (the legacy Array acceptor comma-splits `KEY:VALUE` arguments).
99
+
100
+ ```bash
101
+ gem exec -g kubernetes_template_rendering render_templates \
102
+ --rendered-directory path/to/resources \
103
+ --variable-override-json '{"ephemeral":{"mysql":{"enabled":true}}}' \
104
+ deployment/templates
105
+ ```
106
+
107
+ **Precedence:** all override flags are folded into one hash in command-line order via deep merge (later flags win on conflict), then deep-merged last over the fully-resolved variables.
108
+
109
+ Overrides are echoed once to stdout at render start (`Variable overrides (deep-merged after definitions.yaml): {...}`). Rendered files carry no override comment (removed in [0.3.0](CHANGELOG.md#030---2026-06-24) to avoid content-free diffs on every deploy).
110
+
74
111
  ### Staging Partial Platforms
75
112
 
76
113
  Pass `--spp NAME` (repeatable) to expand any entry whose `definitions.yaml` name contains `SPP-PLACEHOLDER` into a per-SPP sibling output. Substitutes `SPP-PLACEHOLDER` with `NAME` and the `PLACEHOLDER` suffix with the suffix of `NAME` (everything after the last `-`), in both file paths and contents. Source mtimes are preserved.
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  require_relative "template_directory_renderer"
4
6
  require_relative "cli_arguments"
7
+ require_relative "variable_override_parser"
5
8
 
6
9
  module KubernetesTemplateRendering
7
10
  class CLI
@@ -43,21 +46,30 @@ module KubernetesTemplateRendering
43
46
  args.only << name
44
47
  end
45
48
 
46
- op.on("--variable-override=KEY:VALUE", "override a variable value set within definitions.yaml", Array) do |overrides|
49
+ op.on("--variable-override=KEY:VALUE", "override a variable value set within definitions.yaml (KEY may be a dotted path, e.g. components.webServer.hpa.minReplicas:2; escape literal dots as \\.)", Array) do |overrides|
47
50
  args.variable_overrides ||= {} # Initialize as a Hash
48
51
  overrides.each do |override|
49
- key, value = override.split(":", 2)
50
- args.variable_overrides[key] = value if key && value
52
+ VariableOverrideParser.merge_override!(args.variable_overrides, override)
51
53
  end
52
54
  end
53
55
 
56
+ op.on("--variable-override-json=JSON", "deep-merge a JSON object of variable overrides (repeatable; later flags win)") do |json|
57
+ args.variable_overrides ||= {}
58
+ VariableOverrideParser.merge_json!(args.variable_overrides, json)
59
+ end
60
+
54
61
  op.on("-h", "--help") do
55
62
  puts op
56
63
  exit
57
64
  end
58
65
  end
59
66
 
60
- parser.parse!(options)
67
+ begin
68
+ parser.parse!(options)
69
+ rescue VariableOverrideParser::ParseError => ex
70
+ STDERR.puts(ex.message)
71
+ exit(1)
72
+ end
61
73
  args.template_directory = options.first
62
74
  args.spps = (args.spps || []).uniq
63
75
  args.only = (args.only || []).uniq
@@ -80,6 +92,10 @@ module KubernetesTemplateRendering
80
92
  exit(1)
81
93
  end
82
94
 
95
+ if args.variable_overrides&.any?
96
+ puts "Variable overrides (deep-merged after definitions.yaml): #{JSON.generate(args.variable_overrides)}"
97
+ end
98
+
83
99
  [renderer_from_args(args), args]
84
100
  end
85
101
 
@@ -74,12 +74,13 @@ module KubernetesTemplateRendering
74
74
 
75
75
  # Bounded reconcile scope roots for this entry, one per region × color.
76
76
  #
77
- # The sweep root is the parent directory of the entry's rendered `output_directory`, derived
78
- # from the actual `directory:` pattern rather than assuming a fixed layout. For a
79
- # `.../<service>` pattern that parent is the `<region>/<cluster_type>/<color>` base; for a
80
- # `<region>/<service>` pattern it is `<region>`. The parent is shared with sibling entries, so
81
- # leftovers from a deleted/renamed entry under it are swept. The renderer validates each
82
- # `base_root` stays within `rendered_directory` (out-of-prefix, full or relative, is a hard error).
77
+ # The sweep root is the entry's canonical base `<region>/<cluster_type>/<color>` for a non-SPP
78
+ # entry, or `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER` for an SPP one independent of
79
+ # how deep the `subdirectory:` nests. Anchoring at the canonical base (rather than the rendered
80
+ # `output_directory`'s parent) keeps the sweep root fixed when a `subdirectory:` is renamed or
81
+ # nested deeper, so leftovers at the old shallower path are still swept. The renderer validates
82
+ # that the `output_directory` actually falls within this root before sweeping (an entry that
83
+ # renders elsewhere — only reachable via the deprecated `directory:` field — is a hard error).
83
84
  #
84
85
  # Each scope also carries `spp:` (whether this entry is an SPP definition) and `spp_base_root:`
85
86
  # (the canonical `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER` prefix for that region ×
@@ -88,8 +89,10 @@ module KubernetesTemplateRendering
88
89
  @regions.flat_map do |plain_region|
89
90
  @colors.map do |c|
90
91
  output_directory = File.join(@rendered_directory, format(@target_output_directory, plain_region: plain_region, color: c, type: @kubernetes_cluster_type))
92
+ base_path = File.join(@rendered_directory, format(BASE_OUTPUT_DIRECTORY, plain_region: plain_region, color: c, type: @kubernetes_cluster_type))
91
93
  spp_base_root = File.join(@rendered_directory, format(SPP_BASE_OUTPUT_DIRECTORY, plain_region: plain_region, color: c, type: @kubernetes_cluster_type))
92
- { base_root: File.dirname(output_directory), output_directory: output_directory, spp: @spp, spp_base_root: spp_base_root }
94
+ base_root = @spp ? spp_base_root : base_path
95
+ { base_root: base_root, output_directory: output_directory, spp: @spp, spp_base_root: spp_base_root }
93
96
  end
94
97
  end
95
98
  end
@@ -4,6 +4,7 @@ require "ostruct"
4
4
  require "open3"
5
5
  require "shellwords"
6
6
  require "yaml"
7
+ require "active_support/core_ext/hash/deep_merge"
7
8
 
8
9
  # This is a base class for all Templates. Derived classes must implement the render method.
9
10
  module KubernetesTemplateRendering
@@ -12,7 +13,7 @@ module KubernetesTemplateRendering
12
13
 
13
14
  def initialize(template_path, variables, source_repo: nil, variable_overrides: {})
14
15
  @template_path = template_path
15
- @variables = variables.merge(variable_overrides)
16
+ @variables = variables.deep_merge(variable_overrides)
16
17
  @source_repo = source_repo
17
18
  @variable_overrides = variable_overrides
18
19
  end
@@ -92,6 +92,11 @@ module KubernetesTemplateRendering
92
92
  scopes.each do |scope|
93
93
  validate_within_scope!(scope[:base_root], @rendered_directory)
94
94
  validate_spp_layout!(scope)
95
+ # Every entry must render within its canonical root so reconcile owns exactly what it sweeps.
96
+ # SPP entries are already constrained to the spp/SPP-PLACEHOLDER prefix by validate_spp_layout!;
97
+ # a non-SPP entry can only escape its base via the deprecated `directory:` field, which reconcile
98
+ # cannot own — so reject it before any writes rather than sweep a tree it does not render into.
99
+ validate_within_scope!(scope[:output_directory], scope[:base_root]) unless scope[:spp]
95
100
  end
96
101
 
97
102
  base_roots = []
@@ -142,7 +147,8 @@ module KubernetesTemplateRendering
142
147
  # is always re-rendered (it is the expansion source) and each target is freshly expanded from it,
143
148
  # so both are marker-safe. Unrequested SPP siblings are not re-rendered this run and stay excluded.
144
149
  def spp_reconcile_roots(scope)
145
- root = spp_sweep_root(scope)
150
+ # base_root is the canonical `.../spp/SPP-PLACEHOLDER` prefix for SPP entries.
151
+ root = scope[:base_root]
146
152
  return [root] if @spps.empty?
147
153
  return [root] unless root.include?(ResourceSet::SPP_PLACEHOLDER)
148
154
 
@@ -153,13 +159,6 @@ module KubernetesTemplateRendering
153
159
  Pathname.new(root).relative_path_from(Pathname.new(@rendered_directory)).each_filename.include?(SPP_FENCE_DIRNAME)
154
160
  end
155
161
 
156
- # When the directory pattern has no service subdirectory (e.g. `.../spp/SPP-PLACEHOLDER`),
157
- # base_root lands at the `spp/` level itself — sweeping that would touch all SPP siblings.
158
- # Use output_directory (= `spp/<spp-name>`) in that case instead.
159
- def spp_sweep_root(scope)
160
- File.basename(scope[:base_root]) == SPP_FENCE_DIRNAME ? scope[:output_directory] : scope[:base_root]
161
- end
162
-
163
162
  def reconcile_sweep(reconciler, scopes)
164
163
  scopes[:base_roots].each { |r| reconciler.sweep!(root: r, fences: [File.join(r, SPP_FENCE_DIRNAME)]) }
165
164
  scopes[:spp_roots].each { |r| reconciler.sweep!(root: r) }
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "active_support/core_ext/hash/deep_merge"
5
+
6
+ module KubernetesTemplateRendering
7
+ # Parses --variable-override / --variable-override-json values into (possibly nested)
8
+ # override fragments and deep-merges them, in command-line order, into one overrides hash.
9
+ #
10
+ # Key forms:
11
+ # * Legacy: KEY contains no "." -> top-level key, value kept as the raw string
12
+ # (exactly the pre-0.7.0 behavior; never type-coerced).
13
+ # * Dotted: KEY contains "." -> split on unescaped dots into a nested path ("\." is a
14
+ # literal dot within a segment). The value is JSON-coerced when it parses as JSON
15
+ # (2 -> Integer, true/false -> booleans, null -> nil, "2" -> String), else kept raw.
16
+ class VariableOverrideParser
17
+ class ParseError < StandardError; end
18
+
19
+ UNESCAPED_DOT = /(?<!\\)\./.freeze
20
+
21
+ class << self
22
+ # Deep-merges a KEY:VALUE override fragment into +overrides+.
23
+ #
24
+ # @param overrides [Hash] accumulated overrides hash (mutated in place)
25
+ # @param raw [String] raw KEY:VALUE argument
26
+ # @return [Hash] +overrides+ after merging
27
+ def merge_override!(overrides, raw)
28
+ key, value = raw.split(":", 2)
29
+ return overrides if key.nil? || value.nil? # preserve legacy silent-skip of colon-less args
30
+
31
+ overrides.deep_merge!(fragment_for(key, value))
32
+ end
33
+
34
+ # Deep-merges a JSON object string into +overrides+.
35
+ #
36
+ # @param overrides [Hash] accumulated overrides hash (mutated in place)
37
+ # @param json [String] JSON object string
38
+ # @return [Hash] +overrides+ after merging
39
+ # @raise [ParseError] when +json+ is invalid or not a JSON object
40
+ def merge_json!(overrides, json)
41
+ fragment =
42
+ begin
43
+ JSON.parse(json)
44
+ rescue JSON::ParserError => ex
45
+ raise ParseError, "--variable-override-json value is not valid JSON: #{ex.message}"
46
+ end
47
+ fragment.is_a?(Hash) or raise ParseError, "--variable-override-json value must be a JSON object, got #{fragment.class}: #{json.inspect}"
48
+ overrides.deep_merge!(fragment)
49
+ end
50
+
51
+ private
52
+
53
+ def fragment_for(key, value)
54
+ if key.include?(".")
55
+ nest(path_segments(key), coerce(value))
56
+ else
57
+ { key => value } # legacy: literal key, raw string value
58
+ end
59
+ end
60
+
61
+ def path_segments(key)
62
+ segments = key.split(UNESCAPED_DOT, -1)
63
+ segments.none?(&:empty?) or
64
+ raise ParseError, "--variable-override key #{key.inspect} has an empty path segment (leading, trailing, or doubled '.')"
65
+ segments.map { |segment| segment.gsub("\\.", ".") }
66
+ end
67
+
68
+ def nest(segments, value)
69
+ segments.reverse.reduce(value) { |acc, segment| { segment => acc } }
70
+ end
71
+
72
+ def coerce(value)
73
+ JSON.parse(value)
74
+ rescue JSON::ParserError
75
+ value
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KubernetesTemplateRendering
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kubernetes_template_rendering
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Octothorpe
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-08 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -101,6 +101,7 @@ files:
101
101
  - lib/kubernetes_template_rendering/resource_set.rb
102
102
  - lib/kubernetes_template_rendering/template.rb
103
103
  - lib/kubernetes_template_rendering/template_directory_renderer.rb
104
+ - lib/kubernetes_template_rendering/variable_override_parser.rb
104
105
  - lib/kubernetes_template_rendering/version.rb
105
106
  - mkdocs.yml
106
107
  - sig/kubernetes_template_rendering.rbs