kdep 0.4.7 → 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 +4 -4
- data/lib/kdep/commands/bump.rb +15 -0
- data/lib/kdep/commands/check.rb +14 -0
- data/lib/kdep/commands/restart.rb +40 -12
- data/lib/kdep/doctor/check_yaml_dup_keys.rb +27 -0
- data/lib/kdep/doctor.rb +2 -0
- data/lib/kdep/state.rb +33 -1
- data/lib/kdep/version.rb +1 -1
- data/lib/kdep/yaml_duplicate_keys.rb +72 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ed0d8de8dfb58cc372615340a068aecb3f8092aecafdbc6d1c6480c14477d57
|
|
4
|
+
data.tar.gz: 8dde50b29b917321144fcca1d0cb83cff8b66a702442c5bf19375ae527f4cf6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3669fafcc6315fcdb7af39bb0b31cb7314a953b3185988523af63e1d9ab1fb60a458ba141dc22e3735e00de5df8a821b862b657aaae68d36e8326e0a0f304f2b
|
|
7
|
+
data.tar.gz: 7cbb7445b6909b46e74f47245c88df5fa8d7d93617a949b14059aef3f50c2981941ed129d70f013dede5e2789ad363e55d8768698129059c412c03d631851b81
|
data/lib/kdep/commands/bump.rb
CHANGED
|
@@ -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
|
|
|
@@ -270,6 +271,20 @@ module Kdep
|
|
|
270
271
|
end
|
|
271
272
|
end
|
|
272
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
|
+
|
|
273
288
|
def run_preflight_checks(config, deploy_dir, kdep_dir)
|
|
274
289
|
checks = config["check"]
|
|
275
290
|
return if checks.nil? || checks.empty?
|
data/lib/kdep/commands/check.rb
CHANGED
|
@@ -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)
|
|
@@ -3,16 +3,16 @@ require "optparse"
|
|
|
3
3
|
module Kdep
|
|
4
4
|
module Commands
|
|
5
5
|
class Restart
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -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/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
|
-
|
|
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
|
@@ -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
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: envspec
|
|
@@ -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,6 +145,7 @@ 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
|