kdep 0.3.5 → 0.4.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 +4 -4
- data/lib/kdep/check_runner.rb +52 -0
- data/lib/kdep/cli.rb +1 -0
- data/lib/kdep/commands/bump.rb +101 -15
- data/lib/kdep/commands/check.rb +84 -0
- data/lib/kdep/commands/helm_install.rb +90 -7
- data/lib/kdep/config.rb +24 -0
- data/lib/kdep/configmap_overlay.rb +110 -0
- data/lib/kdep/doctor/check_state_gitignored.rb +4 -2
- data/lib/kdep/doctor.rb +0 -2
- data/lib/kdep/helm.rb +10 -3
- data/lib/kdep/kubectl.rb +30 -10
- data/lib/kdep/old_format.rb +12 -6
- data/lib/kdep/path_resolver.rb +61 -0
- data/lib/kdep/renderer.rb +22 -0
- data/lib/kdep/rollout_tracker.rb +62 -0
- data/lib/kdep/version.rb +1 -1
- data/lib/kdep/writer.rb +12 -0
- data/lib/kdep.rb +5 -0
- data/templates/resources/helm_ingress.yml.erb +43 -0
- metadata +7 -2
- data/lib/kdep/doctor/check_ancestor_ignores_state.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09613eb122fcc0b4963a04b0d2d982dbd3b4a6d7c3325aebebdfad3875eb6d69'
|
|
4
|
+
data.tar.gz: 1d844ed77f42b6b50fdbdeaf3aece71e78658223571f827d5c31a9713361c4f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15743ca9ed3ced89956e3b83535629d127f0c7b51378b54653d3dc4c62b4c799f63e5c28fdf760ac1c327173656741a69a8efe10b8821c4a4536e9ba06c8b11e
|
|
7
|
+
data.tar.gz: edfc8dae8a811f72fc12563f5832c253b4637e6da02bfe134b1aed19ed0ce2e830697d8631b74e18cb63d6e811b9941e5598ff915e33ac58928cb2304cf19863
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
# Runs declarative preflight validators from app.yml's `check:` list.
|
|
5
|
+
# Each entry is `cmd: <shell-string>` plus optional `inputs: [paths]`. Paths
|
|
6
|
+
# are resolved via PathResolver. The full invocation is `sh -c "<cmd>
|
|
7
|
+
# <resolved_inputs...>"`. Any non-zero exit halts the pipeline.
|
|
8
|
+
class CheckRunner
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# Injectable runner so tests can assert argv without shelling out.
|
|
12
|
+
class << self
|
|
13
|
+
attr_accessor :runner
|
|
14
|
+
end
|
|
15
|
+
self.runner = ->(cmd) { Open3.capture3("sh", "-c", cmd) }
|
|
16
|
+
|
|
17
|
+
def initialize(checks:, path_resolver:, ui:)
|
|
18
|
+
@checks = checks || []
|
|
19
|
+
@resolver = path_resolver
|
|
20
|
+
@ui = ui
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run!
|
|
24
|
+
return true if @checks.empty?
|
|
25
|
+
|
|
26
|
+
@checks.each do |entry|
|
|
27
|
+
cmd = entry["cmd"].to_s
|
|
28
|
+
raise Error, "check entry missing 'cmd'" if cmd.empty?
|
|
29
|
+
inputs = Array(entry["inputs"]).map { |p| @resolver.resolve(p, warn_on_fallback: true) }
|
|
30
|
+
# Shell-quote each input so paths with spaces survive.
|
|
31
|
+
quoted_inputs = inputs.compact.map { |p| shell_quote(p) }
|
|
32
|
+
full_cmd = ([cmd] + quoted_inputs).join(" ")
|
|
33
|
+
|
|
34
|
+
@ui.info("check: #{full_cmd}")
|
|
35
|
+
stdout, stderr, status = self.class.runner.call(full_cmd)
|
|
36
|
+
unless status.success?
|
|
37
|
+
@ui.error("check failed: #{full_cmd}")
|
|
38
|
+
@ui.error(stderr.strip) unless stderr.strip.empty?
|
|
39
|
+
@ui.error(stdout.strip) unless stdout.strip.empty?
|
|
40
|
+
raise Error, "preflight check failed: #{full_cmd}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def shell_quote(str)
|
|
49
|
+
"'#{str.to_s.gsub("'", %q('\\\\''))}'"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/kdep/cli.rb
CHANGED
data/lib/kdep/commands/bump.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Kdep
|
|
|
14
14
|
opts.on("--no-dashboard", "Skip TUI dashboard after deploy")
|
|
15
15
|
opts.on("--platform=PLATFORM", "Target platform (e.g., linux/amd64)")
|
|
16
16
|
opts.on("--init-state", "Seed state.yml at 0.0 when missing (non-interactive)")
|
|
17
|
+
opts.on("--no-check", "Skip preflight check: validators")
|
|
17
18
|
end
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -44,7 +45,43 @@ module Kdep
|
|
|
44
45
|
|
|
45
46
|
config = Kdep::Config.new(deploy_dir, env).load
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
+
# Feature G: preflight validators. Runs for every preset, halts on
|
|
49
|
+
# first failure. --no-check skips.
|
|
50
|
+
unless @command_options[:"no-check"]
|
|
51
|
+
run_preflight_checks(config, deploy_dir, kdep_dir)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Feature E: dispatch on preset before the docker-heavy app pipeline.
|
|
55
|
+
# Helm and custom presets don't build/push images — they route to
|
|
56
|
+
# preset-specific pipelines that keep the rest of bump's guarantees
|
|
57
|
+
# (context guard, rollout tracking, etc.).
|
|
58
|
+
preset = config["preset"]
|
|
59
|
+
if preset == "helm"
|
|
60
|
+
run_helm_pipeline(deploy_dir, env)
|
|
61
|
+
return
|
|
62
|
+
elsif preset == "custom"
|
|
63
|
+
run_custom_pipeline(deploy_dir, config, kdep_dir)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Step 2: Local config sanity — read state.yml BEFORE touching the
|
|
68
|
+
# cluster. A missing/corrupt state.yml is a local config error and
|
|
69
|
+
# should fail fast without waking kubectl.
|
|
70
|
+
begin
|
|
71
|
+
current_tag = Kdep::State.tag(deploy_dir) || handle_missing_state(deploy_dir)
|
|
72
|
+
rescue Kdep::State::Error => e
|
|
73
|
+
@ui.error(e.message)
|
|
74
|
+
exit 1
|
|
75
|
+
end
|
|
76
|
+
# to_fix.md #3 — warn if someone has gitignored state.yml
|
|
77
|
+
require "kdep/doctor/check_state_gitignored"
|
|
78
|
+
gitignore_result = Kdep::Doctor::CheckStateGitignored.new(deploy_dir).run
|
|
79
|
+
if gitignore_result.severity == :error
|
|
80
|
+
@ui.warn(gitignore_result.message)
|
|
81
|
+
@ui.warn(gitignore_result.hint) if gitignore_result.hint
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Step 3: Context guard (bump touches the cluster)
|
|
48
85
|
begin
|
|
49
86
|
Kdep::ContextGuard.new(config["context"]).validate!
|
|
50
87
|
rescue Kdep::Kubectl::Error => e
|
|
@@ -64,20 +101,7 @@ module Kdep
|
|
|
64
101
|
repo_root = File.dirname(kdep_dir)
|
|
65
102
|
|
|
66
103
|
begin
|
|
67
|
-
# Step
|
|
68
|
-
begin
|
|
69
|
-
current_tag = Kdep::State.tag(deploy_dir) || handle_missing_state(deploy_dir)
|
|
70
|
-
rescue Kdep::State::Error => e
|
|
71
|
-
@ui.error(e.message)
|
|
72
|
-
exit 1
|
|
73
|
-
end
|
|
74
|
-
# to_fix.md #3 — warn if someone has gitignored state.yml
|
|
75
|
-
require "kdep/doctor/check_state_gitignored"
|
|
76
|
-
gitignore_result = Kdep::Doctor::CheckStateGitignored.new(deploy_dir).run
|
|
77
|
-
if gitignore_result.severity == :error
|
|
78
|
-
@ui.warn(gitignore_result.message)
|
|
79
|
-
@ui.warn(gitignore_result.hint) if gitignore_result.hint
|
|
80
|
-
end
|
|
104
|
+
# Step 4: Increment minor version.
|
|
81
105
|
parsed = Kdep::VersionTagger.parse(current_tag)
|
|
82
106
|
next_version = parsed ? "#{parsed[0]}.#{parsed[1] + 1}" : "0.1"
|
|
83
107
|
@ui.info("#{current_tag} -> #{next_version}")
|
|
@@ -192,6 +216,68 @@ module Kdep
|
|
|
192
216
|
|
|
193
217
|
private
|
|
194
218
|
|
|
219
|
+
# Feature E: helm-preset bump = helm-install pipeline. Preflight
|
|
220
|
+
# already ran; context guard runs inside HelmInstall; rollout flush
|
|
221
|
+
# and configmap overlays are internal to it.
|
|
222
|
+
def run_helm_pipeline(deploy_dir, env)
|
|
223
|
+
deploy_arg = File.basename(deploy_dir)
|
|
224
|
+
helm_install = Kdep::Commands::HelmInstall.new(
|
|
225
|
+
global_options: @global_options,
|
|
226
|
+
command_options: { :"dry-run" => @command_options[:"dry-run"] },
|
|
227
|
+
args: env ? [deploy_arg, env] : [deploy_arg],
|
|
228
|
+
)
|
|
229
|
+
helm_install.execute
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Feature E: custom-preset bump = render + apply, no docker, no state.
|
|
233
|
+
def run_custom_pipeline(deploy_dir, config, kdep_dir)
|
|
234
|
+
# Context guard for safety.
|
|
235
|
+
begin
|
|
236
|
+
Kdep::ContextGuard.new(config["context"]).validate!
|
|
237
|
+
rescue Kdep::Kubectl::Error => e
|
|
238
|
+
@ui.error(e.message)
|
|
239
|
+
exit 1
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
namespace = config["namespace"]
|
|
243
|
+
repo_root = File.dirname(kdep_dir)
|
|
244
|
+
output_dir = File.join(deploy_dir, ".rendered")
|
|
245
|
+
|
|
246
|
+
writer = Kdep::Writer.new(output_dir)
|
|
247
|
+
writer.clean
|
|
248
|
+
renderer = Kdep::Renderer.new(config, deploy_dir)
|
|
249
|
+
preset_resources = Kdep::Preset.new("custom", deploy_dir).resources
|
|
250
|
+
|
|
251
|
+
preset_resources.each_with_index do |resource, idx|
|
|
252
|
+
content = renderer.render_resource(resource)
|
|
253
|
+
path = writer.write(resource, content, idx + 1)
|
|
254
|
+
@ui.file_written(path.sub(repo_root + "/", "")) if path
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
unless @command_options[:"dry-run"]
|
|
258
|
+
Dir.glob(File.join(output_dir, "*.yml")).sort.each do |file_path|
|
|
259
|
+
Kdep::Kubectl.apply(file_path, namespace: namespace)
|
|
260
|
+
@ui.info("applied: #{File.basename(file_path)}")
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def run_preflight_checks(config, deploy_dir, kdep_dir)
|
|
266
|
+
checks = config["check"]
|
|
267
|
+
return if checks.nil? || checks.empty?
|
|
268
|
+
|
|
269
|
+
resolver = Kdep::PathResolver.new(
|
|
270
|
+
deploy_dir: deploy_dir,
|
|
271
|
+
kdep_dir: kdep_dir,
|
|
272
|
+
paths_from: config["paths_from"],
|
|
273
|
+
)
|
|
274
|
+
begin
|
|
275
|
+
Kdep::CheckRunner.new(checks: checks, path_resolver: resolver, ui: @ui).run!
|
|
276
|
+
rescue Kdep::CheckRunner::Error
|
|
277
|
+
exit 1
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
195
281
|
def handle_missing_state(deploy_dir)
|
|
196
282
|
if @command_options[:"init-state"]
|
|
197
283
|
seed_initial_state(deploy_dir, reason: :flag)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
module Commands
|
|
5
|
+
# `kdep check <deploy>` — runs preflight validators declared in app.yml's
|
|
6
|
+
# check: list. Exits 0 on success, non-zero on first failing validator.
|
|
7
|
+
class Check
|
|
8
|
+
def self.option_parser
|
|
9
|
+
OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: kdep check [deploy] [env]"
|
|
11
|
+
opts.separator ""
|
|
12
|
+
opts.separator "Runs preflight validators (promtool, amtool, etc.) declared in app.yml's"
|
|
13
|
+
opts.separator "check: list. Invoked automatically by kdep bump unless --no-check is set."
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(global_options:, command_options:, args:)
|
|
18
|
+
@global_options = global_options
|
|
19
|
+
@command_options = command_options
|
|
20
|
+
@args = args
|
|
21
|
+
@ui = Kdep::UI.new(color: false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute
|
|
25
|
+
deploy_name = @args[0]
|
|
26
|
+
env = @args[1]
|
|
27
|
+
|
|
28
|
+
discovery = Kdep::Discovery.new
|
|
29
|
+
kdep_dir = discovery.find_kdep_dir
|
|
30
|
+
unless kdep_dir
|
|
31
|
+
@ui.error("No kdep/ directory found")
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
|
|
36
|
+
exit 1 unless deploy_dir
|
|
37
|
+
|
|
38
|
+
config = Kdep::Config.new(deploy_dir, env).load
|
|
39
|
+
checks = config["check"]
|
|
40
|
+
if checks.nil? || checks.empty?
|
|
41
|
+
@ui.info("no check: entries declared for this deploy")
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
resolver = Kdep::PathResolver.new(
|
|
46
|
+
deploy_dir: deploy_dir,
|
|
47
|
+
kdep_dir: kdep_dir,
|
|
48
|
+
paths_from: config["paths_from"],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
Kdep::CheckRunner.new(checks: checks, path_resolver: resolver, ui: @ui).run!
|
|
53
|
+
@ui.success("all #{checks.size} checks passed")
|
|
54
|
+
rescue Kdep::CheckRunner::Error
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
|
|
62
|
+
if deploy_name
|
|
63
|
+
path = File.join(kdep_dir, deploy_name)
|
|
64
|
+
unless File.directory?(path)
|
|
65
|
+
@ui.error("Deploy target not found: #{deploy_name}")
|
|
66
|
+
return nil
|
|
67
|
+
end
|
|
68
|
+
path
|
|
69
|
+
else
|
|
70
|
+
deploys = discovery.find_deploys
|
|
71
|
+
if deploys.length == 1
|
|
72
|
+
File.join(kdep_dir, deploys[0])
|
|
73
|
+
elsif deploys.length > 1
|
|
74
|
+
@ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
|
|
75
|
+
nil
|
|
76
|
+
else
|
|
77
|
+
@ui.error("No deploy targets found in kdep/")
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -46,6 +46,18 @@ module Kdep
|
|
|
46
46
|
# Load config
|
|
47
47
|
config = Kdep::Config.new(@deploy_dir, env).load
|
|
48
48
|
@namespace = config["namespace"]
|
|
49
|
+
@kdep_dir = kdep_dir
|
|
50
|
+
@resolver = Kdep::PathResolver.new(
|
|
51
|
+
deploy_dir: @deploy_dir,
|
|
52
|
+
kdep_dir: @kdep_dir,
|
|
53
|
+
paths_from: config["paths_from"],
|
|
54
|
+
)
|
|
55
|
+
@tracker = Kdep::RolloutTracker.new(
|
|
56
|
+
rollout_on: config["rollout_on"],
|
|
57
|
+
namespace: @namespace,
|
|
58
|
+
ui: @ui,
|
|
59
|
+
dry_run: @dry_run,
|
|
60
|
+
)
|
|
49
61
|
|
|
50
62
|
unless config["preset"] == "helm"
|
|
51
63
|
@ui.error("#{deploy_name} is not a helm preset (preset: #{config["preset"]})")
|
|
@@ -88,10 +100,43 @@ module Kdep
|
|
|
88
100
|
install_chart(chart_conf)
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
# Feature A: overlay rich config into helm-owned configmaps.
|
|
104
|
+
apply_configmap_overlays(config)
|
|
105
|
+
|
|
91
106
|
# Render and apply additional k8s resources (ingress, secret, etc.)
|
|
92
107
|
apply_extra_resources(config, kdep_dir)
|
|
108
|
+
|
|
109
|
+
# Feature F: flush rollout-restarts after all apply phases complete.
|
|
110
|
+
ok = @tracker.flush!
|
|
111
|
+
exit 1 if @tracker.errors? && !ok
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def apply_configmap_overlays(config)
|
|
117
|
+
overlays = config["configmap_overlays"] || []
|
|
118
|
+
return if overlays.empty?
|
|
119
|
+
|
|
120
|
+
overlays.each do |overlay_hash|
|
|
121
|
+
overlay = Kdep::ConfigmapOverlay.new(
|
|
122
|
+
overlay_hash,
|
|
123
|
+
namespace: @namespace,
|
|
124
|
+
path_resolver: @resolver,
|
|
125
|
+
ui: @ui,
|
|
126
|
+
dry_run: @dry_run,
|
|
127
|
+
)
|
|
128
|
+
begin
|
|
129
|
+
result = overlay.apply!
|
|
130
|
+
@tracker.record_applied("configmap", overlay_hash["name"]) if result&.applied
|
|
131
|
+
rescue Kdep::ConfigmapOverlay::Error => e
|
|
132
|
+
@ui.error(e.message)
|
|
133
|
+
exit 1
|
|
134
|
+
end
|
|
135
|
+
end
|
|
93
136
|
end
|
|
94
137
|
|
|
138
|
+
public
|
|
139
|
+
|
|
95
140
|
private
|
|
96
141
|
|
|
97
142
|
# Build a normalized list of chart configs.
|
|
@@ -108,16 +153,22 @@ module Kdep
|
|
|
108
153
|
{
|
|
109
154
|
release: c["release"] || config["name"],
|
|
110
155
|
chart: c["chart"],
|
|
156
|
+
version: c["version"],
|
|
111
157
|
values: c["values"],
|
|
112
158
|
sets: c["sets"] || {},
|
|
113
159
|
}
|
|
114
160
|
end
|
|
115
161
|
elsif config["chart"]
|
|
162
|
+
sets = config["sets"] || config["helm_sets"] || {}
|
|
163
|
+
if config.key?("helm_sets") && !config.key?("sets")
|
|
164
|
+
@ui.warn("helm_sets: is deprecated; use sets: (will be removed in kdep 0.5.x)")
|
|
165
|
+
end
|
|
116
166
|
[{
|
|
117
167
|
release: config["release"] || config["name"],
|
|
118
168
|
chart: config["chart"],
|
|
169
|
+
version: config["version"],
|
|
119
170
|
values: nil, # auto-detect values.yml in deploy dir
|
|
120
|
-
sets:
|
|
171
|
+
sets: sets,
|
|
121
172
|
}]
|
|
122
173
|
else
|
|
123
174
|
[]
|
|
@@ -137,7 +188,9 @@ module Kdep
|
|
|
137
188
|
sets[key] = interpolate(value.to_s, @secrets)
|
|
138
189
|
end
|
|
139
190
|
|
|
140
|
-
|
|
191
|
+
version = chart_conf[:version]
|
|
192
|
+
version_suffix = version ? " --version #{version}" : ""
|
|
193
|
+
@ui.info("helm upgrade --install #{release} #{chart} -n #{@namespace}#{version_suffix}")
|
|
141
194
|
@ui.info(" values: #{values_file}") if values_file
|
|
142
195
|
sets.each { |k, v| @ui.info(" --set #{k}=#{mask(v)}") }
|
|
143
196
|
|
|
@@ -146,6 +199,7 @@ module Kdep
|
|
|
146
199
|
release: release,
|
|
147
200
|
chart: chart,
|
|
148
201
|
namespace: @namespace,
|
|
202
|
+
version: version,
|
|
149
203
|
values_file: values_file,
|
|
150
204
|
sets: sets,
|
|
151
205
|
dry_run: @dry_run
|
|
@@ -160,13 +214,14 @@ module Kdep
|
|
|
160
214
|
|
|
161
215
|
def resolve_values_file(explicit_name)
|
|
162
216
|
if explicit_name
|
|
163
|
-
path =
|
|
164
|
-
return path if File.exist?(path)
|
|
217
|
+
path = @resolver.resolve(explicit_name, warn_on_fallback: true)
|
|
218
|
+
return path if path && File.exist?(path)
|
|
165
219
|
@ui.warn("Values file not found: #{explicit_name}")
|
|
166
220
|
return nil
|
|
167
221
|
end
|
|
168
222
|
|
|
169
|
-
# Auto-detect values.yml or values.yaml
|
|
223
|
+
# Auto-detect values.yml or values.yaml in the deploy dir (not affected
|
|
224
|
+
# by paths_from: repo_root, since auto-detect is a deploy-dir contract).
|
|
170
225
|
%w[values.yml values.yaml].each do |name|
|
|
171
226
|
path = File.join(@deploy_dir, name)
|
|
172
227
|
return path if File.exist?(path)
|
|
@@ -190,8 +245,35 @@ module Kdep
|
|
|
190
245
|
files_written = 0
|
|
191
246
|
render_errors = []
|
|
192
247
|
|
|
193
|
-
|
|
194
|
-
|
|
248
|
+
next_index = 1
|
|
249
|
+
resources.each do |resource_name|
|
|
250
|
+
# Feature B: multi-ingress replaces the single built-in ingress when
|
|
251
|
+
# config["ingresses"] is set.
|
|
252
|
+
if resource_name == "ingress" && config["ingresses"]
|
|
253
|
+
begin
|
|
254
|
+
entries = renderer.render_helm_ingresses
|
|
255
|
+
rescue => e
|
|
256
|
+
render_errors << "ingress: #{e.message}"
|
|
257
|
+
next
|
|
258
|
+
end
|
|
259
|
+
entries.each do |suffix, content|
|
|
260
|
+
unless content.nil? || content.strip.empty?
|
|
261
|
+
result = validator.validate(content, "ingress")
|
|
262
|
+
unless result["valid"]
|
|
263
|
+
result["errors"].each { |err| render_errors << "ingress[#{suffix}]: #{err}" }
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
path = writer.write_suffixed("ingress", suffix, content, next_index)
|
|
267
|
+
if path
|
|
268
|
+
@ui.file_written(path.sub(repo_root + "/", ""))
|
|
269
|
+
files_written += 1
|
|
270
|
+
next_index += 1
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
next
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
index = next_index
|
|
195
277
|
begin
|
|
196
278
|
content = renderer.render_resource(resource_name)
|
|
197
279
|
rescue => e
|
|
@@ -212,6 +294,7 @@ module Kdep
|
|
|
212
294
|
if path
|
|
213
295
|
@ui.file_written(path.sub(repo_root + "/", ""))
|
|
214
296
|
files_written += 1
|
|
297
|
+
next_index += 1
|
|
215
298
|
end
|
|
216
299
|
end
|
|
217
300
|
|
data/lib/kdep/config.rb
CHANGED
|
@@ -64,7 +64,31 @@ module Kdep
|
|
|
64
64
|
result["image"] = result["name"]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
desugar_configmap_overlay_rollout!(result)
|
|
68
|
+
|
|
67
69
|
result
|
|
68
70
|
end
|
|
71
|
+
|
|
72
|
+
# Feature F sugar: `configmap_overlays[].rollout` becomes an entry in
|
|
73
|
+
# `rollout_on` with source: configmap/<overlay-name>. If an explicit
|
|
74
|
+
# rollout_on entry already exists for that source, merge targets (union,
|
|
75
|
+
# deduped). Canonical internal form is always `rollout_on`.
|
|
76
|
+
def desugar_configmap_overlay_rollout!(config)
|
|
77
|
+
overlays = config["configmap_overlays"]
|
|
78
|
+
return unless overlays.is_a?(Array)
|
|
79
|
+
|
|
80
|
+
overlays.each do |overlay|
|
|
81
|
+
next unless overlay.is_a?(Hash) && overlay["rollout"]
|
|
82
|
+
src = "configmap/#{overlay["name"]}"
|
|
83
|
+
target = overlay["rollout"]
|
|
84
|
+
config["rollout_on"] ||= []
|
|
85
|
+
existing = config["rollout_on"].find { |r| r.is_a?(Hash) && r["source"] == src }
|
|
86
|
+
if existing
|
|
87
|
+
existing["targets"] = (Array(existing["targets"]) + [target]).uniq
|
|
88
|
+
else
|
|
89
|
+
config["rollout_on"] << { "source" => src, "targets" => [target] }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
69
93
|
end
|
|
70
94
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Kdep
|
|
5
|
+
# Overlays rich configmap data from external files onto a helm-owned
|
|
6
|
+
# configmap. Runs after `helm upgrade --install` so the chart creates the
|
|
7
|
+
# configmap first; this module rewrites its data keys without touching
|
|
8
|
+
# helm-set labels or annotations.
|
|
9
|
+
#
|
|
10
|
+
# Two merge strategies:
|
|
11
|
+
# replace — fetch live configmap, replace `data:` with only the listed
|
|
12
|
+
# keys, `kubectl apply` the full manifest back.
|
|
13
|
+
# patch — emit a JSON patch that adds/updates only the listed keys,
|
|
14
|
+
# leaving any other data helm wrote intact.
|
|
15
|
+
#
|
|
16
|
+
# First-deploy safety: if the configmap does not yet exist (chart hasn't
|
|
17
|
+
# run yet), the overlay is skipped with a warning; a re-run picks it up.
|
|
18
|
+
class ConfigmapOverlay
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
Result = Struct.new(:configmap_name, :applied, :skipped_reason,
|
|
22
|
+
keyword_init: true)
|
|
23
|
+
|
|
24
|
+
def initialize(overlay, namespace:, path_resolver:, ui:, dry_run: false)
|
|
25
|
+
@overlay = overlay
|
|
26
|
+
@namespace = namespace
|
|
27
|
+
@resolver = path_resolver
|
|
28
|
+
@ui = ui
|
|
29
|
+
@dry_run = dry_run
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def name
|
|
33
|
+
@overlay["name"]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def merge_strategy
|
|
37
|
+
(@overlay["merge"] || "replace").to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def apply!
|
|
41
|
+
case merge_strategy
|
|
42
|
+
when "replace" then apply_replace!
|
|
43
|
+
when "patch" then apply_patch!
|
|
44
|
+
else
|
|
45
|
+
raise Error, "configmap_overlay #{name}: unknown merge strategy '#{merge_strategy}' (expected replace|patch)"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def data_payload
|
|
52
|
+
payload = {}
|
|
53
|
+
@overlay["data_files"].each do |key, path|
|
|
54
|
+
resolved = @resolver.resolve(path, warn_on_fallback: true)
|
|
55
|
+
raise Error, "configmap_overlay #{name}: source file not found: #{path}" unless resolved && File.exist?(resolved)
|
|
56
|
+
content = File.read(resolved)
|
|
57
|
+
raise Error, "configmap_overlay #{name}: source file is empty: #{path}" if content.empty?
|
|
58
|
+
payload[key] = content
|
|
59
|
+
end
|
|
60
|
+
payload
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def apply_replace!
|
|
64
|
+
live_yaml = Kdep::Kubectl.get("configmap", name, namespace: @namespace)
|
|
65
|
+
if live_yaml.nil? || live_yaml.strip.empty?
|
|
66
|
+
@ui.warn("configmap_overlay #{name}: not yet created (first deploy); skipping")
|
|
67
|
+
return Result.new(configmap_name: name, applied: false, skipped_reason: "not_yet_created")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
live = Kdep::YAMLCompat.safe_load(live_yaml)
|
|
71
|
+
raise Error, "configmap_overlay #{name}: unexpected live configmap shape" unless live.is_a?(Hash)
|
|
72
|
+
live["data"] = data_payload
|
|
73
|
+
# Strip server-side-managed fields that confuse kubectl apply.
|
|
74
|
+
if live["metadata"].is_a?(Hash)
|
|
75
|
+
%w[resourceVersion uid creationTimestamp managedFields selfLink generation].each { |k| live["metadata"].delete(k) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
require "tempfile"
|
|
79
|
+
tmp = Tempfile.new(["#{name}-overlay", ".yml"])
|
|
80
|
+
tmp.write(YAML.dump(live))
|
|
81
|
+
tmp.close
|
|
82
|
+
|
|
83
|
+
if @dry_run
|
|
84
|
+
@ui.info("[dry-run] would apply configmap_overlay #{name}")
|
|
85
|
+
else
|
|
86
|
+
Kdep::Kubectl.apply(tmp.path, namespace: @namespace)
|
|
87
|
+
@ui.info("configmap_overlay #{name}: applied (replace)")
|
|
88
|
+
end
|
|
89
|
+
Result.new(configmap_name: name, applied: true, skipped_reason: nil)
|
|
90
|
+
ensure
|
|
91
|
+
tmp&.unlink
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_patch!
|
|
95
|
+
ops = data_payload.map { |k, v| { "op" => "add", "path" => "/data/#{escape_patch_key(k)}", "value" => v } }
|
|
96
|
+
if @dry_run
|
|
97
|
+
@ui.info("[dry-run] would patch configmap_overlay #{name} (#{ops.size} keys)")
|
|
98
|
+
else
|
|
99
|
+
Kdep::Kubectl.patch("configmap", name, namespace: @namespace, type: "json", patch: ops.to_json)
|
|
100
|
+
@ui.info("configmap_overlay #{name}: patched (#{ops.size} keys)")
|
|
101
|
+
end
|
|
102
|
+
Result.new(configmap_name: name, applied: true, skipped_reason: nil)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# RFC 6901 JSON-pointer escaping for `/` and `~` in key names.
|
|
106
|
+
def escape_patch_key(key)
|
|
107
|
+
key.to_s.gsub("~", "~0").gsub("/", "~1")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -8,12 +8,14 @@ module Kdep
|
|
|
8
8
|
|
|
9
9
|
def run
|
|
10
10
|
state_path = File.join(deploy_dir, Kdep::State::FILENAME)
|
|
11
|
-
|
|
11
|
+
# capture3 swallows stderr so "fatal: not a git repository" noise
|
|
12
|
+
# never reaches the user when the caller runs outside a repo.
|
|
13
|
+
_, _, status = Open3.capture3("git", "check-ignore", "-q", state_path)
|
|
12
14
|
if status.success?
|
|
13
15
|
Result.new(
|
|
14
16
|
id: ID, severity: :error,
|
|
15
17
|
message: "#{state_path} is gitignored",
|
|
16
|
-
hint: "Remove 'state.yml' from .gitignore and 'git add' the file so fresh clones see the last-deployed tag."
|
|
18
|
+
hint: "Remove 'state.yml' from any .gitignore (local or ancestor) and 'git add' the file so fresh clones see the last-deployed tag."
|
|
17
19
|
)
|
|
18
20
|
else
|
|
19
21
|
Result.ok(ID)
|
data/lib/kdep/doctor.rb
CHANGED
|
@@ -5,7 +5,6 @@ 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_ancestor_ignores_state"
|
|
9
8
|
|
|
10
9
|
module Kdep
|
|
11
10
|
module Doctor
|
|
@@ -15,7 +14,6 @@ module Kdep
|
|
|
15
14
|
CheckStateTagFormat,
|
|
16
15
|
CheckStateGitignored,
|
|
17
16
|
CheckGitignoreCanonical,
|
|
18
|
-
CheckAncestorIgnoresState,
|
|
19
17
|
].freeze
|
|
20
18
|
end
|
|
21
19
|
end
|
data/lib/kdep/helm.rb
CHANGED
|
@@ -4,17 +4,24 @@ module Kdep
|
|
|
4
4
|
module Helm
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :runner
|
|
9
|
+
end
|
|
10
|
+
# Default: shell out to the real helm binary.
|
|
11
|
+
self.runner = ->(*args) { Open3.capture3("helm", *args) }
|
|
12
|
+
|
|
7
13
|
def self.run(*args)
|
|
8
|
-
stdout, stderr, status =
|
|
14
|
+
stdout, stderr, status = runner.call(*args)
|
|
9
15
|
unless status.success?
|
|
10
16
|
raise Error, "helm #{args.first} failed: #{stderr.strip}"
|
|
11
17
|
end
|
|
12
18
|
stdout
|
|
13
19
|
end
|
|
14
20
|
|
|
15
|
-
# helm upgrade --install <release> <chart> -n <namespace> [-f values.yml] [--set k=v ...]
|
|
16
|
-
def self.upgrade_install(release:, chart:, namespace:, values_file: nil, sets: {}, dry_run: false)
|
|
21
|
+
# helm upgrade --install <release> <chart> -n <namespace> [--version <v>] [-f values.yml] [--set k=v ...]
|
|
22
|
+
def self.upgrade_install(release:, chart:, namespace:, version: nil, values_file: nil, sets: {}, dry_run: false)
|
|
17
23
|
args = ["upgrade", "--install", release, chart, "-n", namespace]
|
|
24
|
+
args += ["--version", version] if version
|
|
18
25
|
args += ["-f", values_file] if values_file
|
|
19
26
|
sets.each do |key, value|
|
|
20
27
|
args += ["--set", "#{key}=#{value}"]
|
data/lib/kdep/kubectl.rb
CHANGED
|
@@ -5,8 +5,14 @@ module Kdep
|
|
|
5
5
|
module Kubectl
|
|
6
6
|
class Error < StandardError; end
|
|
7
7
|
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :runner
|
|
10
|
+
end
|
|
11
|
+
# Default: shell out to the real kubectl binary.
|
|
12
|
+
self.runner = ->(*args) { Open3.capture3("kubectl", *args) }
|
|
13
|
+
|
|
8
14
|
def self.run(*args)
|
|
9
|
-
stdout, stderr, status =
|
|
15
|
+
stdout, stderr, status = runner.call(*args)
|
|
10
16
|
unless status.success?
|
|
11
17
|
raise Error, "kubectl #{args.first} failed: #{stderr.strip}"
|
|
12
18
|
end
|
|
@@ -14,8 +20,7 @@ module Kdep
|
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
def self.run_json(*args)
|
|
17
|
-
|
|
18
|
-
JSON.parse(output)
|
|
23
|
+
JSON.parse(run(*args, "-o", "json"))
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def self.current_context
|
|
@@ -27,15 +32,30 @@ module Kdep
|
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def self.diff(file_path)
|
|
30
|
-
stdout, stderr, status =
|
|
35
|
+
stdout, stderr, status = runner.call("diff", "-f", file_path)
|
|
31
36
|
case status.exitstatus
|
|
32
|
-
when 0
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
stdout
|
|
36
|
-
else
|
|
37
|
-
raise Error, "kubectl diff failed (exit #{status.exitstatus}): #{stderr.strip}"
|
|
37
|
+
when 0 then nil # no diff
|
|
38
|
+
when 1 then stdout # diffs present on stdout
|
|
39
|
+
else raise Error, "kubectl diff failed (exit #{status.exitstatus}): #{stderr.strip}"
|
|
38
40
|
end
|
|
39
41
|
end
|
|
42
|
+
|
|
43
|
+
# Declarative rollout-restart helper used by Kdep::RolloutTracker (Feature F).
|
|
44
|
+
def self.rollout_restart(kind, name, namespace:)
|
|
45
|
+
run("rollout", "restart", "#{kind}/#{name}", "-n", namespace)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# JSON-patch helper used by Kdep::ConfigmapOverlay (Feature A) with merge: patch.
|
|
49
|
+
def self.patch(kind, name, namespace:, type:, patch:)
|
|
50
|
+
run("patch", kind, name, "-n", namespace, "--type", type, "-p", patch)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Live-fetch helper. Returns nil when the resource is absent (NotFound).
|
|
54
|
+
# Caller distinguishes "not yet created" from real errors.
|
|
55
|
+
def self.get(kind, name, namespace:, output: "yaml")
|
|
56
|
+
run("get", kind, name, "-n", namespace, "-o", output)
|
|
57
|
+
rescue Error
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
40
60
|
end
|
|
41
61
|
end
|
data/lib/kdep/old_format.rb
CHANGED
|
@@ -261,14 +261,20 @@ module Kdep
|
|
|
261
261
|
# Extract the image tag from the live deploy (e.g., "0.44" from
|
|
262
262
|
# "ghcr.io/org/app:0.44"). Returns nil if the tag is "latest",
|
|
263
263
|
# empty, or absent. On multi-container deployments, returns the
|
|
264
|
-
# first usable tag found.
|
|
264
|
+
# first usable tag found. Uses rpartition so registry URLs with
|
|
265
|
+
# explicit ports (host:5000/org/app:0.44) aren't misparsed.
|
|
265
266
|
def extracted_tag
|
|
266
267
|
deployed_images.each do |img|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
before, sep, after = img.to_s.rpartition(":")
|
|
269
|
+
# rpartition returns ["", "", img] when no ":" exists
|
|
270
|
+
next if sep.empty?
|
|
271
|
+
# A host:port with no tag (e.g. "host:5000/img") has sep but the
|
|
272
|
+
# "tag" part contains a "/" — not a real tag.
|
|
273
|
+
next if after.empty? || after.include?("/")
|
|
274
|
+
next if after == "latest"
|
|
275
|
+
# before will include any host:port prefix intact
|
|
276
|
+
_ = before
|
|
277
|
+
return after
|
|
272
278
|
end
|
|
273
279
|
nil
|
|
274
280
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
# Resolves user-supplied relative file paths from app.yml against either
|
|
5
|
+
# the deploy dir (default) or the repo root that contains kdep/.
|
|
6
|
+
# Mode is selected by the top-level `paths_from:` field.
|
|
7
|
+
class PathResolver
|
|
8
|
+
def initialize(deploy_dir:, kdep_dir:, paths_from:)
|
|
9
|
+
@deploy_dir = deploy_dir
|
|
10
|
+
@repo_root = File.dirname(kdep_dir)
|
|
11
|
+
@mode = paths_from.to_s == "repo_root" ? :repo_root : :target
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the absolute filesystem path for a relative input. If the
|
|
15
|
+
# resolved path does not exist at the configured origin but DOES exist at
|
|
16
|
+
# the other origin, resolves at the other and optionally warns — this
|
|
17
|
+
# catches the common "forgot paths_from: repo_root" mistake.
|
|
18
|
+
def resolve(path, warn_on_fallback: false)
|
|
19
|
+
return nil if path.nil?
|
|
20
|
+
return path if Pathname.new(path).absolute?
|
|
21
|
+
|
|
22
|
+
primary = File.expand_path(path, primary_base)
|
|
23
|
+
return primary if File.exist?(primary)
|
|
24
|
+
|
|
25
|
+
fallback = File.expand_path(path, fallback_base)
|
|
26
|
+
if File.exist?(fallback)
|
|
27
|
+
if warn_on_fallback
|
|
28
|
+
warn "path '#{path}' not found under #{@mode}; falling back to #{other_mode}. " \
|
|
29
|
+
"Set paths_from: #{other_mode} or fix the path."
|
|
30
|
+
end
|
|
31
|
+
return fallback
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
primary
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Checks either origin. Used by validators that don't care WHICH origin
|
|
38
|
+
# holds the file, only that it exists somewhere sensible.
|
|
39
|
+
def exists?(path)
|
|
40
|
+
return false if path.nil?
|
|
41
|
+
return File.exist?(path) if Pathname.new(path).absolute?
|
|
42
|
+
|
|
43
|
+
File.exist?(File.expand_path(path, primary_base)) ||
|
|
44
|
+
File.exist?(File.expand_path(path, fallback_base))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def primary_base
|
|
50
|
+
@mode == :repo_root ? @repo_root : @deploy_dir
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def fallback_base
|
|
54
|
+
@mode == :repo_root ? @deploy_dir : @repo_root
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def other_mode
|
|
58
|
+
@mode == :repo_root ? :target : :repo_root
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/kdep/renderer.rb
CHANGED
|
@@ -28,6 +28,28 @@ module Kdep
|
|
|
28
28
|
erb.result(context.get_binding)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Feature B: render N Ingress resources from config["ingresses"].
|
|
32
|
+
# Returns [[suffix, yaml], ...] so the caller can emit multiple files.
|
|
33
|
+
def render_helm_ingresses
|
|
34
|
+
entries = @config["ingresses"] || []
|
|
35
|
+
return [] if entries.empty?
|
|
36
|
+
|
|
37
|
+
template_path = File.join(Kdep.templates_dir, "resources", "helm_ingress.yml.erb")
|
|
38
|
+
raise "helm_ingress template not found at #{template_path}" unless File.exist?(template_path)
|
|
39
|
+
template_content = File.read(template_path)
|
|
40
|
+
erb = if RUBY_VERSION >= "2.6"
|
|
41
|
+
ERB.new(template_content, trim_mode: "-")
|
|
42
|
+
else
|
|
43
|
+
ERB.new(template_content, nil, "-")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
entries.map do |ingress|
|
|
47
|
+
context = TemplateContext.new(@config)
|
|
48
|
+
context.instance_variable_set(:@ingress, ingress)
|
|
49
|
+
[ingress["name"], erb.result(context.get_binding)]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
31
53
|
private
|
|
32
54
|
|
|
33
55
|
def resolve_template(resource_name)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
# Declarative rollout-restart triggers. Phases that apply resources register
|
|
5
|
+
# sources; at end of run the tracker looks up matching rollout_on rules and
|
|
6
|
+
# restarts the unique set of targets once.
|
|
7
|
+
#
|
|
8
|
+
# sources are "kind/name" (configmap, secret).
|
|
9
|
+
# targets are "kind/name" (deployment, statefulset, daemonset).
|
|
10
|
+
class RolloutTracker
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
def initialize(rollout_on:, namespace:, ui:, dry_run: false)
|
|
14
|
+
@rules = rollout_on || []
|
|
15
|
+
@namespace = namespace
|
|
16
|
+
@ui = ui
|
|
17
|
+
@dry_run = dry_run
|
|
18
|
+
@applied = Set.new
|
|
19
|
+
@errors = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_applied(kind, name)
|
|
23
|
+
@applied << "#{kind.to_s.downcase}/#{name}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def flush!
|
|
27
|
+
targets = Set.new
|
|
28
|
+
@rules.each do |rule|
|
|
29
|
+
src = normalize(rule["source"])
|
|
30
|
+
next unless @applied.include?(src)
|
|
31
|
+
Array(rule["targets"]).each { |t| targets << normalize(t) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
targets.sort.each do |target|
|
|
35
|
+
kind, name = target.split("/", 2)
|
|
36
|
+
if @dry_run
|
|
37
|
+
@ui.info("[dry-run] would rollout-restart #{kind}/#{name}")
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
begin
|
|
41
|
+
Kdep::Kubectl.rollout_restart(kind, name, namespace: @namespace)
|
|
42
|
+
@ui.info("rollout-restarted #{kind}/#{name}")
|
|
43
|
+
rescue Kdep::Kubectl::Error => e
|
|
44
|
+
@errors << "#{kind}/#{name}: #{e.message}"
|
|
45
|
+
@ui.error("rollout-restart failed for #{kind}/#{name}: #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@errors.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def errors?
|
|
53
|
+
!@errors.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize(s)
|
|
59
|
+
s.to_s.downcase
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/kdep/version.rb
CHANGED
data/lib/kdep/writer.rb
CHANGED
|
@@ -22,5 +22,17 @@ module Kdep
|
|
|
22
22
|
File.write(path, content)
|
|
23
23
|
path
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# Used by Feature B (multi-ingress): one logical resource "ingress"
|
|
27
|
+
# emits N files named NN-ingress-<suffix>.yml so kubectl apply diffs
|
|
28
|
+
# them independently.
|
|
29
|
+
def write_suffixed(base_name, suffix, content, index)
|
|
30
|
+
return nil if content.nil? || content.strip.empty?
|
|
31
|
+
FileUtils.mkdir_p(@output_dir)
|
|
32
|
+
filename = format("%02d-%s-%s.yml", index, base_name, suffix)
|
|
33
|
+
path = File.join(@output_dir, filename)
|
|
34
|
+
File.write(path, content)
|
|
35
|
+
path
|
|
36
|
+
end
|
|
25
37
|
end
|
|
26
38
|
end
|
data/lib/kdep.rb
CHANGED
|
@@ -18,6 +18,10 @@ require "kdep/helm"
|
|
|
18
18
|
require "kdep/registry"
|
|
19
19
|
require "kdep/version_tagger"
|
|
20
20
|
require "kdep/state"
|
|
21
|
+
require "kdep/path_resolver"
|
|
22
|
+
require "kdep/configmap_overlay"
|
|
23
|
+
require "kdep/rollout_tracker"
|
|
24
|
+
require "kdep/check_runner"
|
|
21
25
|
require "kdep/doctor"
|
|
22
26
|
require "kdep/commands/render"
|
|
23
27
|
require "kdep/commands/init"
|
|
@@ -37,6 +41,7 @@ require "kdep/commands/migrate"
|
|
|
37
41
|
require "kdep/commands/helm_install"
|
|
38
42
|
require "kdep/commands/dashboard"
|
|
39
43
|
require "kdep/commands/doctor"
|
|
44
|
+
require "kdep/commands/check"
|
|
40
45
|
require "kdep/dashboard/screen"
|
|
41
46
|
require "kdep/dashboard/layout"
|
|
42
47
|
require "kdep/dashboard/panel"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<%
|
|
2
|
+
raise "helm_ingress template requires @ingress (single entry)" unless @ingress
|
|
3
|
+
ingress = @ingress
|
|
4
|
+
ingress_name = "ingress-#{ingress["name"]}"
|
|
5
|
+
default_annotations = {
|
|
6
|
+
"kubernetes.io/ingress.class" => "nginx",
|
|
7
|
+
"cert-manager.io/cluster-issuer" => "letsencrypt",
|
|
8
|
+
}
|
|
9
|
+
if ingress["auth_secret"] && !ingress["auth_secret"].to_s.empty?
|
|
10
|
+
default_annotations["nginx.ingress.kubernetes.io/auth-type"] = "basic"
|
|
11
|
+
default_annotations["nginx.ingress.kubernetes.io/auth-secret"] = ingress["auth_secret"]
|
|
12
|
+
default_annotations["nginx.ingress.kubernetes.io/auth-realm"] = "Auth required"
|
|
13
|
+
end
|
|
14
|
+
user_annotations = ingress["annotations"].is_a?(Hash) ? ingress["annotations"] : {}
|
|
15
|
+
annotations = default_annotations.merge(user_annotations)
|
|
16
|
+
path = ingress["path"] || "/"
|
|
17
|
+
path_type = ingress["path_type"] || "Prefix"
|
|
18
|
+
-%>
|
|
19
|
+
apiVersion: networking.k8s.io/v1
|
|
20
|
+
kind: Ingress
|
|
21
|
+
metadata:
|
|
22
|
+
name: <%= ingress_name %>
|
|
23
|
+
namespace: <%= namespace %>
|
|
24
|
+
annotations:
|
|
25
|
+
<% annotations.each do |k, v| -%>
|
|
26
|
+
<%= k %>: "<%= v %>"
|
|
27
|
+
<% end -%>
|
|
28
|
+
spec:
|
|
29
|
+
tls:
|
|
30
|
+
- hosts:
|
|
31
|
+
- <%= ingress["host"] %>
|
|
32
|
+
secretName: <%= ingress["tls_secret"] %>
|
|
33
|
+
rules:
|
|
34
|
+
- host: <%= ingress["host"] %>
|
|
35
|
+
http:
|
|
36
|
+
paths:
|
|
37
|
+
- path: <%= path %>
|
|
38
|
+
pathType: <%= path_type %>
|
|
39
|
+
backend:
|
|
40
|
+
service:
|
|
41
|
+
name: <%= ingress["service"] %>
|
|
42
|
+
port:
|
|
43
|
+
number: <%= ingress["port"] %>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kdep
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leadfy
|
|
@@ -63,11 +63,13 @@ files:
|
|
|
63
63
|
- LICENSE.txt
|
|
64
64
|
- exe/kdep
|
|
65
65
|
- lib/kdep.rb
|
|
66
|
+
- lib/kdep/check_runner.rb
|
|
66
67
|
- lib/kdep/cli.rb
|
|
67
68
|
- lib/kdep/cluster_health.rb
|
|
68
69
|
- lib/kdep/commands/apply.rb
|
|
69
70
|
- lib/kdep/commands/build.rb
|
|
70
71
|
- lib/kdep/commands/bump.rb
|
|
72
|
+
- lib/kdep/commands/check.rb
|
|
71
73
|
- lib/kdep/commands/dashboard.rb
|
|
72
74
|
- lib/kdep/commands/diff.rb
|
|
73
75
|
- lib/kdep/commands/doctor.rb
|
|
@@ -84,6 +86,7 @@ files:
|
|
|
84
86
|
- lib/kdep/commands/sh.rb
|
|
85
87
|
- lib/kdep/commands/status.rb
|
|
86
88
|
- lib/kdep/config.rb
|
|
89
|
+
- lib/kdep/configmap_overlay.rb
|
|
87
90
|
- lib/kdep/context_guard.rb
|
|
88
91
|
- lib/kdep/dashboard.rb
|
|
89
92
|
- lib/kdep/dashboard/health_panel.rb
|
|
@@ -98,7 +101,6 @@ files:
|
|
|
98
101
|
- lib/kdep/docker.rb
|
|
99
102
|
- lib/kdep/doctor.rb
|
|
100
103
|
- lib/kdep/doctor/check.rb
|
|
101
|
-
- lib/kdep/doctor/check_ancestor_ignores_state.rb
|
|
102
104
|
- lib/kdep/doctor/check_gitignore_canonical.rb
|
|
103
105
|
- lib/kdep/doctor/check_state_gitignored.rb
|
|
104
106
|
- lib/kdep/doctor/check_state_parseable.rb
|
|
@@ -108,9 +110,11 @@ files:
|
|
|
108
110
|
- lib/kdep/helm.rb
|
|
109
111
|
- lib/kdep/kubectl.rb
|
|
110
112
|
- lib/kdep/old_format.rb
|
|
113
|
+
- lib/kdep/path_resolver.rb
|
|
111
114
|
- lib/kdep/preset.rb
|
|
112
115
|
- lib/kdep/registry.rb
|
|
113
116
|
- lib/kdep/renderer.rb
|
|
117
|
+
- lib/kdep/rollout_tracker.rb
|
|
114
118
|
- lib/kdep/state.rb
|
|
115
119
|
- lib/kdep/template_context.rb
|
|
116
120
|
- lib/kdep/ui.rb
|
|
@@ -134,6 +138,7 @@ files:
|
|
|
134
138
|
- templates/resources/configmap.yml.erb
|
|
135
139
|
- templates/resources/cronjob.yml.erb
|
|
136
140
|
- templates/resources/deployment.yml.erb
|
|
141
|
+
- templates/resources/helm_ingress.yml.erb
|
|
137
142
|
- templates/resources/ingress.yml.erb
|
|
138
143
|
- templates/resources/job.yml.erb
|
|
139
144
|
- templates/resources/secret.yml.erb
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
require "open3"
|
|
2
|
-
require "kdep/state"
|
|
3
|
-
|
|
4
|
-
module Kdep
|
|
5
|
-
module Doctor
|
|
6
|
-
class CheckAncestorIgnoresState < Check
|
|
7
|
-
ID = "ancestor_ignores_state".freeze
|
|
8
|
-
|
|
9
|
-
def run
|
|
10
|
-
state_path = File.join(deploy_dir, Kdep::State::FILENAME)
|
|
11
|
-
_, status = Open3.capture2("git", "check-ignore", "-v", state_path)
|
|
12
|
-
if status.success?
|
|
13
|
-
Result.new(
|
|
14
|
-
id: ID, severity: :error,
|
|
15
|
-
message: "An ancestor .gitignore is ignoring #{state_path}",
|
|
16
|
-
hint: "Run 'git check-ignore -v #{state_path}' to find which file, then remove the pattern."
|
|
17
|
-
)
|
|
18
|
-
else
|
|
19
|
-
Result.ok(ID)
|
|
20
|
-
end
|
|
21
|
-
rescue Errno::ENOENT
|
|
22
|
-
Result.ok(ID)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|