kdep 0.4.6 → 0.4.8

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: f292fc84fbd868827a225e5a63ec7ef74949f954216c69fd6eac1fb8d431f7ff
4
- data.tar.gz: b3486a1fc6c39ed310d0cf4a138421fcbbf46b2d7b31deb59f403d5d37c62e5c
3
+ metadata.gz: 1ed0d8de8dfb58cc372615340a068aecb3f8092aecafdbc6d1c6480c14477d57
4
+ data.tar.gz: 8dde50b29b917321144fcca1d0cb83cff8b66a702442c5bf19375ae527f4cf6e
5
5
  SHA512:
6
- metadata.gz: 9c5521c97e1c89ab7bd74f94967d735dca9d46e497dbeaa788274138ebe45ce6c28e4f8ecd24bfb1d2a9180ed995e3713bc488c23793b40154b7d98b38f4c71d
7
- data.tar.gz: cb3a4a8b54be7f2852396207eab1971c47f2922b30878f8d26d2b4625e3cb130a5408d6eaa78e1ad06b815e9ac009c4719e992ffad9c18fc9432ee0525df4518
6
+ metadata.gz: 3669fafcc6315fcdb7af39bb0b31cb7314a953b3185988523af63e1d9ab1fb60a458ba141dc22e3735e00de5df8a821b862b657aaae68d36e8326e0a0f304f2b
7
+ data.tar.gz: 7cbb7445b6909b46e74f47245c88df5fa8d7d93617a949b14059aef3f50c2981941ed129d70f013dede5e2789ad363e55d8768698129059c412c03d631851b81
@@ -42,6 +42,11 @@ module Kdep
42
42
  # Load config
43
43
  config = Kdep::Config.new(deploy_dir, env).load
44
44
 
45
+ if config["build"] == false
46
+ @ui.error("#{deploy_dir} declares `build: false` — its image is built elsewhere, so there is nothing to build here.")
47
+ exit 1
48
+ end
49
+
45
50
  # Resolve tag from state.yml — app.yml has no tag field by design;
46
51
  # state.yml is the source of truth for the last bumped version.
47
52
  tag = Kdep::State.tag(deploy_dir)
@@ -48,6 +48,7 @@ module Kdep
48
48
  # Feature G: preflight validators. Runs for every preset, halts on
49
49
  # first failure. --no-check skips.
50
50
  unless @command_options[:"no-check"]
51
+ check_duplicate_yaml_keys(deploy_dir)
51
52
  run_preflight_checks(config, deploy_dir, kdep_dir)
52
53
  end
53
54
 
@@ -59,8 +60,13 @@ module Kdep
59
60
  if preset == "helm"
60
61
  run_helm_pipeline(deploy_dir, env)
61
62
  return
62
- elsif preset == "custom"
63
- run_custom_pipeline(deploy_dir, config, kdep_dir, env)
63
+ elsif preset == "custom" || config["build"] == false
64
+ # `build: false` marks a target whose image is built elsewhere
65
+ # (upstream images: elasticsearch, fluentd, curator...). It takes the
66
+ # same no-docker route as `custom`, but keeps its own preset's
67
+ # resource list -- the point is to reuse web/worker/statefulset/etc
68
+ # without a Dockerfile. Its tag comes from app.yml, not state.yml.
69
+ run_no_build_pipeline(deploy_dir, config, kdep_dir, env)
64
70
  return
65
71
  end
66
72
 
@@ -229,8 +235,11 @@ module Kdep
229
235
  helm_install.execute
230
236
  end
231
237
 
232
- # Feature E: custom-preset bump = render + apply, no docker, no state.
233
- def run_custom_pipeline(deploy_dir, config, kdep_dir, env = nil)
238
+ # Feature E: bump with no image to build = render + apply, no docker, no
239
+ # state.yml. Used by the `custom` preset and by any preset with
240
+ # `build: false`; the only difference is which resource list is rendered,
241
+ # which is why this reads config["preset"] instead of hardcoding one.
242
+ def run_no_build_pipeline(deploy_dir, config, kdep_dir, env = nil)
234
243
  # Context guard for safety.
235
244
  begin
236
245
  Kdep::ContextGuard.new(config["context"]).validate!
@@ -246,7 +255,7 @@ module Kdep
246
255
  writer = Kdep::Writer.new(output_dir)
247
256
  writer.clean
248
257
  renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
249
- preset_resources = Kdep::Preset.new("custom", deploy_dir).resources
258
+ preset_resources = Kdep::Preset.new(config["preset"], deploy_dir).resources
250
259
 
251
260
  preset_resources.each_with_index do |resource, idx|
252
261
  content = renderer.render_resource(resource)
@@ -262,6 +271,20 @@ module Kdep
262
271
  end
263
272
  end
264
273
 
274
+ # Built-in preflight, unlike the user-declared check: list. The error
275
+ # this catches is made by whoever doesn't know the trap exists, so an
276
+ # opt-in guard would only protect the people who already know. Runs
277
+ # before docker and before the cluster; --no-check is the escape hatch.
278
+ def check_duplicate_yaml_keys(deploy_dir)
279
+ require "kdep/yaml_duplicate_keys"
280
+ dups = Kdep::YamlDuplicateKeys.in_dir(deploy_dir)
281
+ return if dups.empty?
282
+
283
+ dups.each { |dup| @ui.error(dup.to_s) }
284
+ @ui.error("Merge the repeated blocks into one, or re-run with --no-check to bypass.")
285
+ exit 1
286
+ end
287
+
265
288
  def run_preflight_checks(config, deploy_dir, kdep_dir)
266
289
  checks = config["check"]
267
290
  return if checks.nil? || checks.empty?
@@ -35,6 +35,10 @@ module Kdep
35
35
  deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
36
36
  exit 1 unless deploy_dir
37
37
 
38
+ # Built-in guard, ahead of the user-declared list: a duplicate key is
39
+ # resolved silently by the parser, so nothing downstream can report it.
40
+ check_duplicate_yaml_keys(deploy_dir)
41
+
38
42
  config = Kdep::Config.new(deploy_dir, env).load
39
43
  checks = config["check"]
40
44
  if checks.nil? || checks.empty?
@@ -58,6 +62,16 @@ module Kdep
58
62
 
59
63
  private
60
64
 
65
+ def check_duplicate_yaml_keys(deploy_dir)
66
+ require "kdep/yaml_duplicate_keys"
67
+ dups = Kdep::YamlDuplicateKeys.in_dir(deploy_dir)
68
+ return if dups.empty?
69
+
70
+ dups.each { |dup| @ui.error(dup.to_s) }
71
+ @ui.error("Merge the repeated blocks into one.")
72
+ exit 1
73
+ end
74
+
61
75
  def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
62
76
  if deploy_name
63
77
  path = File.join(kdep_dir, deploy_name)
@@ -41,6 +41,11 @@ module Kdep
41
41
  # Load config
42
42
  config = Kdep::Config.new(deploy_dir, env).load
43
43
 
44
+ if config["build"] == false
45
+ @ui.error("#{deploy_dir} declares `build: false` — its image lives in an upstream registry, so there is nothing to push.")
46
+ exit 1
47
+ end
48
+
44
49
  # Resolve tag from state.yml — app.yml has no tag field by design;
45
50
  # state.yml is the source of truth for the last bumped version.
46
51
  tag = Kdep::State.tag(deploy_dir)
@@ -3,16 +3,16 @@ require "optparse"
3
3
  module Kdep
4
4
  module Commands
5
5
  class Restart
6
- RESTARTABLE_PRESETS = %w[web worker].freeze
7
- NON_RESTARTABLE_PRESETS = %w[job cronjob].freeze
6
+ WORKLOAD_KINDS = %w[deployment statefulset daemonset].freeze
7
+ NON_RESTARTABLE_KINDS = %w[job cronjob].freeze
8
8
 
9
9
  def self.option_parser
10
10
  OptionParser.new do |opts|
11
11
  opts.banner = "Usage: kdep restart [deploy] [--type TYPE]"
12
12
  opts.separator ""
13
- opts.separator "Triggers a rolling restart of a Deployment or StatefulSet."
13
+ opts.separator "Triggers a rolling restart of a Deployment, StatefulSet or DaemonSet."
14
14
  opts.separator ""
15
- opts.on("--type TYPE", "Resource type: deployment, statefulset (overrides preset)") do |_|
15
+ opts.on("--type TYPE", "Resource type: deployment, statefulset, daemonset (overrides preset)") do |_|
16
16
  # parsed via into: hash
17
17
  end
18
18
  end
@@ -58,30 +58,58 @@ module Kdep
58
58
  preset = config["preset"]
59
59
 
60
60
  # Determine resource type
61
- resource_type = determine_resource_type(preset)
61
+ resource_type = determine_resource_type(preset, deploy_dir)
62
62
 
63
63
  # Execute rollout restart
64
- Kdep::Kubectl.run("rollout", "restart", "#{resource_type}/#{app_name}", "-n", namespace)
64
+ begin
65
+ Kdep::Kubectl.run("rollout", "restart", "#{resource_type}/#{app_name}", "-n", namespace)
66
+ rescue Kdep::Kubectl::Error => e
67
+ @ui.error(e.message)
68
+ @ui.info("Hint: use --type <deployment|statefulset|daemonset> if the workload kind differs from the preset")
69
+ exit 1
70
+ end
65
71
  @ui.info("Restarted #{resource_type}/#{app_name} in #{namespace}")
66
72
  end
67
73
 
68
74
  private
69
75
 
70
- def determine_resource_type(preset)
76
+ # The preset file lists the resources kdep applies, so the workload kind
77
+ # is declared there -- deriving it from the preset *name* got every
78
+ # non-Deployment target wrong (daemonset, statefulset, custom). Reading
79
+ # the list also handles `custom`, whose resources live in the deploy dir.
80
+ def determine_resource_type(preset, deploy_dir)
71
81
  if @command_options[:type]
72
82
  return @command_options[:type]
73
83
  end
74
84
 
75
- resource_type_for_preset(preset)
85
+ resource_type_for_preset(preset, deploy_dir)
76
86
  end
77
87
 
78
- def resource_type_for_preset(preset)
79
- if NON_RESTARTABLE_PRESETS.include?(preset)
80
- @ui.error("#{preset} workloads cannot be restarted (use --type to override)")
88
+ def resource_type_for_preset(preset, deploy_dir)
89
+ resources = preset_resources(preset, deploy_dir)
90
+
91
+ if (kind = resources.find { |r| NON_RESTARTABLE_KINDS.include?(r) })
92
+ @ui.error("#{kind} workloads cannot be restarted (use --type to override)")
81
93
  exit 1
82
94
  end
83
95
 
84
- "deployment"
96
+ found = resources & WORKLOAD_KINDS
97
+
98
+ if found.size > 1
99
+ @ui.error("Multiple workloads in preset '#{preset}': #{found.join(', ')}. Use --type to pick one.")
100
+ exit 1
101
+ end
102
+
103
+ # helm charts declare no workload of their own, and a missing preset is
104
+ # already reported by render/apply -- keep the old guess rather than
105
+ # failing a command that used to work.
106
+ found.first || "deployment"
107
+ end
108
+
109
+ def preset_resources(preset, deploy_dir)
110
+ Kdep::Preset.new(preset, deploy_dir).resources
111
+ rescue StandardError
112
+ []
85
113
  end
86
114
 
87
115
  def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
data/lib/kdep/defaults.rb CHANGED
@@ -13,6 +13,9 @@ module Kdep
13
13
  "cronjob" => {},
14
14
  "statefulset" => {},
15
15
  "statefulset_svc" => {},
16
+ # No replicas override: a DaemonSet has no replicas field at all, so the
17
+ # BASE_DEFAULTS value is simply never read by daemonset.yml.erb.
18
+ "daemonset" => {},
16
19
  "helm" => {},
17
20
  "custom" => {},
18
21
  }.freeze
@@ -0,0 +1,27 @@
1
+ require "kdep/yaml_duplicate_keys"
2
+
3
+ module Kdep
4
+ module Doctor
5
+ # A duplicate key is not catchable by review -- in a 50-line file with the
6
+ # two blocks 30 lines apart, they are invisible. It's machine work, and
7
+ # kdep is the one place in the flow that sees every deploy's YAML.
8
+ class CheckYamlDupKeys < Check
9
+ ID = "yaml_dup_keys".freeze
10
+
11
+ def run
12
+ dups = Kdep::YamlDuplicateKeys.in_dir(deploy_dir)
13
+ return Result.ok(ID) if dups.empty?
14
+
15
+ Result.new(
16
+ id: ID,
17
+ severity: :error,
18
+ message: dups.map(&:to_s).join("; "),
19
+ # Deliberately not wired into --fix: merging two blocks means
20
+ # deciding which value survives when the inner keys conflict, and a
21
+ # wrong automatic merge is worse than the bug -- it looks reviewed.
22
+ hint: "Merge the repeated blocks into one by hand."
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/kdep/doctor.rb CHANGED
@@ -5,6 +5,7 @@ require "kdep/doctor/check_state_parseable"
5
5
  require "kdep/doctor/check_state_tag_format"
6
6
  require "kdep/doctor/check_state_gitignored"
7
7
  require "kdep/doctor/check_gitignore_canonical"
8
+ require "kdep/doctor/check_yaml_dup_keys"
8
9
 
9
10
  module Kdep
10
11
  module Doctor
@@ -14,6 +15,7 @@ module Kdep
14
15
  CheckStateTagFormat,
15
16
  CheckStateGitignored,
16
17
  CheckGitignoreCanonical,
18
+ CheckYamlDupKeys,
17
19
  ].freeze
18
20
  end
19
21
  end
data/lib/kdep/preset.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Kdep
2
2
  class Preset
3
- BUILT_IN = %w[web worker job cronjob statefulset statefulset_svc helm custom].freeze
3
+ BUILT_IN = %w[web worker job cronjob statefulset statefulset_svc daemonset helm custom].freeze
4
4
 
5
5
  def initialize(preset_name, deploy_dir)
6
6
  raise "preset: is required in app.yml" if preset_name.to_s.strip.empty?
data/lib/kdep/state.rb CHANGED
@@ -27,15 +27,47 @@ module Kdep
27
27
  loaded && loaded["tag"]
28
28
  end
29
29
 
30
+ # Rewrites only the `tag:` line, keeping everything else byte-for-byte.
31
+ # The header says "do not edit by hand", but state.yml is the one place a
32
+ # warning sits next to the data it guards -- e.g. a target that shares an
33
+ # image repo with another and must never be re-seeded with --init-state.
34
+ # Rebuilding the file from scratch silently ate those comments on every
35
+ # bump, and a YAML round-trip would too (the parser doesn't carry them).
30
36
  def self.write(deploy_dir, tag:)
31
37
  path = File.join(deploy_dir, FILENAME)
32
38
  tmp = "#{path}.tmp"
33
- content = "#{HEADER}tag: \"#{tag}\"\n"
39
+
40
+ content = if File.exist?(path)
41
+ with_header(replace_tag_line(File.read(path), tag))
42
+ else
43
+ "#{HEADER}tag: \"#{tag}\"\n"
44
+ end
45
+
34
46
  File.write(tmp, content)
35
47
  File.rename(tmp, path)
36
48
  path
37
49
  end
38
50
 
51
+ # Appends when the key is absent (state hand-edited into a corrupt shape)
52
+ # rather than discarding what was there -- `load` already raises on that,
53
+ # so the right move here is to not destroy the evidence.
54
+ def self.replace_tag_line(current, tag)
55
+ line = "tag: \"#{tag}\""
56
+ return current.sub(/^tag:.*$/, line) if current.match?(/^tag:.*$/)
57
+
58
+ "#{current.chomp}\n#{line}\n"
59
+ end
60
+ private_class_method :replace_tag_line
61
+
62
+ # A state.yml committed by hand (bump suggests exactly that when none
63
+ # exists) has no banner; give it one instead of leaving it unmarked.
64
+ def self.with_header(content)
65
+ return content if content.include?(HEADER.lines.first.chomp)
66
+
67
+ "#{HEADER}#{content}"
68
+ end
69
+ private_class_method :with_header
70
+
39
71
  def self.exists?(deploy_dir)
40
72
  File.exist?(File.join(deploy_dir, FILENAME))
41
73
  end
data/lib/kdep/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kdep
2
- VERSION = "0.4.6"
2
+ VERSION = "0.4.8"
3
3
  end
@@ -0,0 +1,72 @@
1
+ require "psych"
2
+
3
+ module Kdep
4
+ # Finds keys declared twice in the same mapping, at any depth.
5
+ #
6
+ # YAML resolves a repeated key by keeping the LAST one and discarding the
7
+ # earlier block ENTIRELY, in silence. Nothing in the pipeline warns: helm
8
+ # template, helm upgrade and kdep all receive the hash already collapsed by
9
+ # the parser. In repleadfy/llm-api2#1 a redis values.yaml declared `metrics:`
10
+ # twice; the first block -- the one redirecting the exporter registry to
11
+ # bitnamilegacy, after Bitnami closed the free repo -- was dropped at parse
12
+ # time, and the wrong image ran in production for 73 days, surviving only on
13
+ # a node's image cache.
14
+ #
15
+ # This walks the Psych AST rather than calling Psych.load, because load has
16
+ # already applied last-one-wins and destroyed the evidence. The AST also
17
+ # gives real line numbers on both sides, works at any depth, and knows that
18
+ # `key: value` text inside a block scalar is data, not a key -- which a grep
19
+ # over the raw file cannot.
20
+ module YamlDuplicateKeys
21
+ Duplicate = Struct.new(:file, :key_path, :first_line, :repeat_line) do
22
+ def to_s
23
+ "#{file}:#{repeat_line} duplicate key '#{key_path}' (first declared at line " \
24
+ "#{first_line}) -- YAML keeps the last block and discards the earlier one"
25
+ end
26
+ end
27
+
28
+ def self.in_file(path)
29
+ ast = Psych.parse_stream(File.read(path))
30
+ scan(ast).map do |dup|
31
+ Duplicate.new(File.basename(path), dup[:path], dup[:first], dup[:repeat])
32
+ end
33
+ rescue Psych::SyntaxError
34
+ # A file that doesn't parse is a different problem, reported elsewhere
35
+ # with better context (Config#load, helm). Silently collapsing is the
36
+ # failure this scanner exists for; don't turn it into a crash here.
37
+ []
38
+ end
39
+
40
+ # Top-level only: everything kdep reads as input lives in the deploy dir
41
+ # root (app.yml, app.<env>.yml, secrets.yml, values.yaml), while nested
42
+ # dirs hold rendered output and templates.
43
+ def self.in_dir(dir)
44
+ Dir.glob(File.join(dir, "*.{yml,yaml}")).sort.flat_map { |f| in_file(f) }
45
+ end
46
+
47
+ def self.scan(node, path = [])
48
+ found = []
49
+
50
+ case node
51
+ when Psych::Nodes::Mapping
52
+ seen = {}
53
+ node.children.each_slice(2) do |key, value|
54
+ name = key.respond_to?(:value) ? key.value : key.to_s
55
+ if seen.key?(name)
56
+ found << {
57
+ :path => (path + [name]).join("."),
58
+ :first => seen[name],
59
+ :repeat => key.start_line + 1,
60
+ }
61
+ end
62
+ seen[name] = key.start_line + 1
63
+ found.concat(scan(value, path + [name]))
64
+ end
65
+ when Psych::Nodes::Sequence, Psych::Nodes::Document, Psych::Nodes::Stream
66
+ node.children.each { |child| found.concat(scan(child, path)) }
67
+ end
68
+
69
+ found
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # daemonset preset: one pod per node (log shippers, node exporters, agents)
2
+ # Order determines kubectl apply sequence
3
+ configmap
4
+ secret
5
+ daemonset
@@ -0,0 +1,120 @@
1
+ apiVersion: apps/v1
2
+ kind: DaemonSet
3
+ metadata:
4
+ name: <%= @config["daemonset_name"] || name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
8
+ spec:
9
+ selector:
10
+ matchLabels:
11
+ app: <%= name %>
12
+ template:
13
+ metadata:
14
+ labels:
15
+ app: <%= name %>
16
+ <% if @config["pod_labels"] -%>
17
+ <% @config["pod_labels"].each do |k, v| -%>
18
+ <%= k %>: "<%= v %>"
19
+ <% end -%>
20
+ <% end -%>
21
+ spec:
22
+ <% if @config["service_account_name"] -%>
23
+ serviceAccountName: <%= @config["service_account_name"] %>
24
+ <% end -%>
25
+ <% if @config["host_network"] -%>
26
+ hostNetwork: <%= @config["host_network"] %>
27
+ <% end -%>
28
+ <% if @config["termination_grace_period"] -%>
29
+ terminationGracePeriodSeconds: <%= @config["termination_grace_period"] %>
30
+ <% end -%>
31
+ <% if @config["node_selector"] -%>
32
+ nodeSelector:
33
+ <%= @config["node_selector"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
34
+ <% end -%>
35
+ <% if @config["tolerations"] -%>
36
+ <%# DaemonSets routinely need to land on control-plane/tainted nodes. -%>
37
+ tolerations:
38
+ <%= @config["tolerations"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
39
+ <% end -%>
40
+ <% if @config["image_pull_secrets"] -%>
41
+ imagePullSecrets:
42
+ - name: <%= @config["image_pull_secrets"] %>
43
+ <% end -%>
44
+ <% if @config["init_containers"] -%>
45
+ initContainers:
46
+ <%= @config["init_containers"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
47
+ <% end -%>
48
+ containers:
49
+ - name: <%= @config["container_name"] || name %>
50
+ image: <%= image_ref %>
51
+ <% if @config["image_pull_policy"] -%>
52
+ imagePullPolicy: <%= @config["image_pull_policy"] %>
53
+ <% end -%>
54
+ <% if @config["command"] -%>
55
+ command: <%= @config["command"].is_a?(Array) ? @config["command"].inspect : "[\"#{@config["command"]}\"]" %>
56
+ <% end -%>
57
+ <% if @config["args"] -%>
58
+ args: <%= @config["args"].is_a?(Array) ? @config["args"].inspect : "[\"#{@config["args"]}\"]" %>
59
+ <% end -%>
60
+ <% if @config["port"] -%>
61
+ ports:
62
+ - containerPort: <%= port %>
63
+ <% if @config["container_port_protocol"] -%>
64
+ protocol: <%= @config["container_port_protocol"] %>
65
+ <% end -%>
66
+ <% end -%>
67
+ <% if @config["security_context"] -%>
68
+ securityContext:
69
+ <%= @config["security_context"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
70
+ <% end -%>
71
+ <% if @config["resources"] -%>
72
+ resources:
73
+ <% if @config["resources"]["requests"] -%>
74
+ requests:
75
+ <% @config["resources"]["requests"].each do |k, v| -%>
76
+ <%= k %>: <%= v.is_a?(Numeric) ? v : "\"#{v}\"" %>
77
+ <% end -%>
78
+ <% end -%>
79
+ <% if @config["resources"]["limits"] -%>
80
+ limits:
81
+ <% @config["resources"]["limits"].each do |k, v| -%>
82
+ <%= k %>: <%= v.is_a?(Numeric) ? v : "\"#{v}\"" %>
83
+ <% end -%>
84
+ <% end -%>
85
+ <% end -%>
86
+ <% if @config["volume_mounts"] -%>
87
+ volumeMounts:
88
+ <% @config["volume_mounts"].each do |vm| -%>
89
+ - name: <%= vm["name"] %>
90
+ mountPath: <%= vm["mountPath"] %>
91
+ <% if vm["readOnly"] -%>
92
+ readOnly: <%= vm["readOnly"] %>
93
+ <% end -%>
94
+ <% if vm["subPath"] -%>
95
+ subPath: <%= vm["subPath"] %>
96
+ <% end -%>
97
+ <% end -%>
98
+ <% end -%>
99
+ <% if @config["container_env"] -%>
100
+ <%# Named container_env, not env: `env:` is a backward-compat alias for -%>
101
+ <%# `configmap:` (see Config#apply_defaults), so it never reaches a template. -%>
102
+ env:
103
+ <%= @config["container_env"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
104
+ <% end -%>
105
+ envFrom:
106
+ <% env_from_list = @config["env_from"] || ["configmap/config-#{name}", "secret/secret-#{name}"] -%>
107
+ <% env_from_list.each do |ref| -%>
108
+ <% type, ref_name = ref.split("/", 2) -%>
109
+ <% if type == "configmap" -%>
110
+ - configMapRef:
111
+ name: <%= ref_name %>
112
+ <% elsif type == "secret" -%>
113
+ - secretRef:
114
+ name: <%= ref_name %>
115
+ <% end -%>
116
+ <% end -%>
117
+ <% if @config["volumes"] -%>
118
+ volumes:
119
+ <%= @config["volumes"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
120
+ <% end -%>
@@ -27,6 +27,10 @@ spec:
27
27
  <% if @config["image_pull_secrets"] -%>
28
28
  imagePullSecrets:
29
29
  - name: <%= @config["image_pull_secrets"] %>
30
+ <% end -%>
31
+ <% if @config["init_containers"] -%>
32
+ initContainers:
33
+ <%= @config["init_containers"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
30
34
  <% end -%>
31
35
  containers:
32
36
  - name: <%= @config["container_name"] || name %>
@@ -106,6 +110,12 @@ spec:
106
110
  subPath: <%= vm["subPath"] %>
107
111
  <% end -%>
108
112
  <% end -%>
113
+ <% end -%>
114
+ <% if @config["container_env"] -%>
115
+ <%# Named container_env, not env: `env:` is a backward-compat alias for -%>
116
+ <%# `configmap:` (see Config#apply_defaults), so it never reaches a template. -%>
117
+ env:
118
+ <%= @config["container_env"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
109
119
  <% end -%>
110
120
  envFrom:
111
121
  <% env_from_list = @config["env_from"] || ["configmap/config-#{name}", "secret/secret-#{name}"] -%>
@@ -123,3 +133,9 @@ spec:
123
133
  volumes:
124
134
  <%= @config["volumes"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
125
135
  <% end -%>
136
+ <% if @config["volume_claim_templates"] -%>
137
+ <%# Sibling of `template:`, not part of the pod spec -- this is the one field -%>
138
+ <%# that makes a StatefulSet a StatefulSet, so it belongs in the built-in. -%>
139
+ volumeClaimTemplates:
140
+ <%= @config["volume_claim_templates"].to_yaml.sub("---\n", "").gsub(/^/, " ").rstrip %>
141
+ <% end -%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdep
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leadfy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-26 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: envspec
@@ -42,14 +42,14 @@ dependencies:
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '5.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
@@ -125,6 +125,7 @@ files:
125
125
  - lib/kdep/doctor/check_state_parseable.rb
126
126
  - lib/kdep/doctor/check_state_present.rb
127
127
  - lib/kdep/doctor/check_state_tag_format.rb
128
+ - lib/kdep/doctor/check_yaml_dup_keys.rb
128
129
  - lib/kdep/doctor/result.rb
129
130
  - lib/kdep/helm.rb
130
131
  - lib/kdep/kubectl.rb
@@ -144,12 +145,14 @@ files:
144
145
  - lib/kdep/version_tagger.rb
145
146
  - lib/kdep/writer.rb
146
147
  - lib/kdep/yaml_compat.rb
148
+ - lib/kdep/yaml_duplicate_keys.rb
147
149
  - templates/init/app.yml.erb
148
150
  - templates/init/env.spec
149
151
  - templates/init/gitignore
150
152
  - templates/init/secrets.yml
151
153
  - templates/presets/cronjob
152
154
  - templates/presets/custom
155
+ - templates/presets/daemonset
153
156
  - templates/presets/helm
154
157
  - templates/presets/job
155
158
  - templates/presets/statefulset
@@ -158,6 +161,7 @@ files:
158
161
  - templates/presets/worker
159
162
  - templates/resources/configmap.yml.erb
160
163
  - templates/resources/cronjob.yml.erb
164
+ - templates/resources/daemonset.yml.erb
161
165
  - templates/resources/deployment.yml.erb
162
166
  - templates/resources/helm_ingress.yml.erb
163
167
  - templates/resources/infisical_secret.yml.erb