kdep 0.4.9 → 0.6.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/commands/apply.rb +5 -0
- data/lib/kdep/commands/bump.rb +60 -2
- data/lib/kdep/commands/dashboard.rb +7 -0
- data/lib/kdep/commands/helm_install.rb +7 -0
- data/lib/kdep/commands/log.rb +11 -27
- data/lib/kdep/commands/restart.rb +4 -16
- data/lib/kdep/dashboard.rb +13 -4
- data/lib/kdep/kubectl.rb +6 -0
- data/lib/kdep/pod_logs.rb +42 -0
- data/lib/kdep/renderer.rb +71 -0
- data/lib/kdep/version.rb +1 -1
- data/lib/kdep/workload.rb +37 -0
- data/lib/kdep.rb +2 -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: 0f5fd6a62da416ea9cdccd6d6370050509846001fec09b066b63ac8c79918106
|
|
4
|
+
data.tar.gz: a792a173492ad869938ad623743af357e1a1ee6a8d7c85d2f591bcde7511d378
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4640ab8f5039e3e5b4a9135a11afe42738659bf41376f38f0db463f13d1086317dc9c9631b2fb378157f86a1a255d7a29a58cdf816fcc0fe1d045c6b537d1d4a
|
|
7
|
+
data.tar.gz: bae5c85f87d19d2320cccbc169a0b08bb5f4b56b565dd81a1002c904c5e5a9d4ac570c053fc88ee6163534051809b72931a0395ff4cc0882127c32ad7126a068
|
data/lib/kdep/commands/apply.rb
CHANGED
|
@@ -71,6 +71,7 @@ module Kdep
|
|
|
71
71
|
writer = Kdep::Writer.new(output_dir)
|
|
72
72
|
writer.clean
|
|
73
73
|
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
74
|
+
Kdep::Renderer.preflight_secrets!(@ui, renderer, resources)
|
|
74
75
|
validator = Kdep::Validator.new
|
|
75
76
|
validation_errors = []
|
|
76
77
|
|
|
@@ -78,6 +79,10 @@ module Kdep
|
|
|
78
79
|
index = idx + 1
|
|
79
80
|
begin
|
|
80
81
|
content = renderer.render_resource(resource_name)
|
|
82
|
+
rescue Kdep::Renderer::MissingSecretsError => e
|
|
83
|
+
@ui.error("#{resource_name}: #{e.message}")
|
|
84
|
+
@ui.error("nada foi aplicado")
|
|
85
|
+
exit 1
|
|
81
86
|
rescue => e
|
|
82
87
|
@ui.error("#{resource_name}: #{e.message}")
|
|
83
88
|
next
|
data/lib/kdep/commands/bump.rb
CHANGED
|
@@ -3,6 +3,10 @@ require "optparse"
|
|
|
3
3
|
module Kdep
|
|
4
4
|
module Commands
|
|
5
5
|
class Bump
|
|
6
|
+
DEFAULT_ROLLOUT_TIMEOUT = "5m".freeze
|
|
7
|
+
# kubectl duration: 90s, 5m, 1h30m
|
|
8
|
+
DURATION_FORMAT = /\A(\d+(ms|s|m|h))+\z/
|
|
9
|
+
|
|
6
10
|
def self.option_parser
|
|
7
11
|
OptionParser.new do |opts|
|
|
8
12
|
opts.banner = "Usage: kdep bump [deploy] [env]"
|
|
@@ -12,6 +16,8 @@ module Kdep
|
|
|
12
16
|
opts.on("--skip-apply", "Build and push only, skip render+apply")
|
|
13
17
|
opts.on("--dry-run", "Run full pipeline but skip kubectl apply")
|
|
14
18
|
opts.on("--no-dashboard", "Skip TUI dashboard after deploy")
|
|
19
|
+
opts.on("--wait", "Wait for the rollout instead of the dashboard; exit 1 if it fails (for scripts/CI)")
|
|
20
|
+
opts.on("--timeout=DURATION", "Rollout wait timeout with --wait, e.g. 90s, 10m (default: #{DEFAULT_ROLLOUT_TIMEOUT}; 0 = forever)")
|
|
15
21
|
opts.on("--platform=PLATFORM", "Target platform (e.g., linux/amd64)")
|
|
16
22
|
opts.on("--init-state", "Seed state.yml at 0.0 when missing (non-interactive)")
|
|
17
23
|
opts.on("--no-check", "Skip preflight check: validators")
|
|
@@ -29,6 +35,14 @@ module Kdep
|
|
|
29
35
|
deploy_name = @args[0]
|
|
30
36
|
env = @args[1]
|
|
31
37
|
|
|
38
|
+
# A malformed duration would only surface in `kubectl rollout status`,
|
|
39
|
+
# after build, push and apply -- reporting a good deploy as failed.
|
|
40
|
+
timeout = @command_options[:timeout]
|
|
41
|
+
if timeout && !(timeout == "0" || timeout.match?(DURATION_FORMAT))
|
|
42
|
+
@ui.error("--timeout=#{timeout}: expected a duration like 90s, 5m or 1h30m")
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
|
|
32
46
|
# Step 1: Discovery + config
|
|
33
47
|
discovery = Kdep::Discovery.new
|
|
34
48
|
kdep_dir = discovery.find_kdep_dir
|
|
@@ -58,6 +72,7 @@ module Kdep
|
|
|
58
72
|
# (context guard, rollout tracking, etc.).
|
|
59
73
|
preset = config["preset"]
|
|
60
74
|
if preset == "helm"
|
|
75
|
+
@ui.warn("--wait is not supported for helm presets; the rollout is not awaited") if @command_options[:wait]
|
|
61
76
|
run_helm_pipeline(deploy_dir, env)
|
|
62
77
|
return
|
|
63
78
|
elsif preset == "custom" || config["build"] == false
|
|
@@ -106,6 +121,12 @@ module Kdep
|
|
|
106
121
|
registry_url = config["registry"]
|
|
107
122
|
repo_root = File.dirname(kdep_dir)
|
|
108
123
|
|
|
124
|
+
# O Secret é conferido ANTES do docker build/push: falha aqui não empurra imagem nem aplica nada.
|
|
125
|
+
unless @command_options[:"skip-apply"]
|
|
126
|
+
Kdep::Renderer.preflight_secrets!(@ui, Kdep::Renderer.new(config, deploy_dir, env: env),
|
|
127
|
+
Kdep::Preset.new(config["preset"], deploy_dir).resources)
|
|
128
|
+
end
|
|
129
|
+
|
|
109
130
|
begin
|
|
110
131
|
# Step 4: Increment minor version.
|
|
111
132
|
parsed = Kdep::VersionTagger.parse(current_tag)
|
|
@@ -198,8 +219,13 @@ module Kdep
|
|
|
198
219
|
# Save new tag to state.yml
|
|
199
220
|
Kdep::State.write(deploy_dir, tag: next_version)
|
|
200
221
|
|
|
201
|
-
#
|
|
202
|
-
|
|
222
|
+
# Follow the rollout: --wait blocks on it (scripts/CI), otherwise
|
|
223
|
+
# the TUI dashboard when there is a terminal to draw it on.
|
|
224
|
+
if @command_options[:wait]
|
|
225
|
+
wait_for_rollout(config, deploy_dir, namespace)
|
|
226
|
+
elsif @command_options[:"no-dashboard"]
|
|
227
|
+
# explicitly opted out
|
|
228
|
+
elsif Kdep::Dashboard.interactive?
|
|
203
229
|
dashboard = Kdep::Dashboard.new(
|
|
204
230
|
"name" => config["name"],
|
|
205
231
|
"namespace" => namespace,
|
|
@@ -207,10 +233,16 @@ module Kdep
|
|
|
207
233
|
"image" => image
|
|
208
234
|
)
|
|
209
235
|
dashboard.run
|
|
236
|
+
else
|
|
237
|
+
@ui.warn("rollout not awaited -- pass --wait to block until it completes (exit 1 if it fails)")
|
|
210
238
|
end
|
|
211
239
|
end
|
|
212
240
|
end
|
|
213
241
|
|
|
242
|
+
rescue Kdep::Renderer::MissingSecretsError => e
|
|
243
|
+
@ui.error(e.message)
|
|
244
|
+
@ui.error("nada foi aplicado (a imagem pode já ter sido enviada ao registry)")
|
|
245
|
+
exit 1
|
|
214
246
|
rescue Kdep::Docker::Error => e
|
|
215
247
|
@ui.error(e.message)
|
|
216
248
|
exit 1
|
|
@@ -222,6 +254,29 @@ module Kdep
|
|
|
222
254
|
|
|
223
255
|
private
|
|
224
256
|
|
|
257
|
+
def wait_for_rollout(config, deploy_dir, namespace)
|
|
258
|
+
deploy = File.basename(deploy_dir)
|
|
259
|
+
resources = Kdep::Workload.preset_resources(config["preset"], deploy_dir)
|
|
260
|
+
kinds = Kdep::Workload.rollout_kinds(resources)
|
|
261
|
+
|
|
262
|
+
if kinds.empty?
|
|
263
|
+
what = Kdep::Workload.batch_kind(resources) || "no deployment/statefulset/daemonset"
|
|
264
|
+
@ui.warn("--wait: preset '#{config["preset"]}' declares #{what}; no rollout to wait for")
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
timeout = @command_options[:timeout] || DEFAULT_ROLLOUT_TIMEOUT
|
|
269
|
+
kinds.each do |kind|
|
|
270
|
+
name = Kdep::Workload.name_for(kind, config)
|
|
271
|
+
@ui.info("waiting for #{kind}/#{name} rollout (timeout #{timeout})...")
|
|
272
|
+
next if Kdep::Kubectl.rollout_status(kind, name, namespace: namespace, timeout: timeout)
|
|
273
|
+
|
|
274
|
+
@ui.error("rollout of #{kind}/#{name} did not complete within #{timeout}. " \
|
|
275
|
+
"Check `kdep status #{deploy}` and `kdep log #{deploy}`.")
|
|
276
|
+
exit 1
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
225
280
|
# Feature E: helm-preset bump = helm-install pipeline. Preflight
|
|
226
281
|
# already ran; context guard runs inside HelmInstall; rollout flush
|
|
227
282
|
# and configmap overlays are internal to it.
|
|
@@ -256,6 +311,7 @@ module Kdep
|
|
|
256
311
|
writer.clean
|
|
257
312
|
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
258
313
|
preset_resources = Kdep::Preset.new(config["preset"], deploy_dir).resources
|
|
314
|
+
Kdep::Renderer.preflight_secrets!(@ui, renderer, preset_resources)
|
|
259
315
|
|
|
260
316
|
preset_resources.each_with_index do |resource, idx|
|
|
261
317
|
content = renderer.render_resource(resource)
|
|
@@ -268,6 +324,8 @@ module Kdep
|
|
|
268
324
|
Kdep::Kubectl.apply(file_path, namespace: namespace)
|
|
269
325
|
@ui.info("applied: #{File.basename(file_path)}")
|
|
270
326
|
end
|
|
327
|
+
|
|
328
|
+
wait_for_rollout(config, deploy_dir, namespace) if @command_options[:wait]
|
|
271
329
|
end
|
|
272
330
|
end
|
|
273
331
|
|
|
@@ -39,6 +39,13 @@ module Kdep
|
|
|
39
39
|
# Load config (nil env -- dashboard monitors live state)
|
|
40
40
|
config = Kdep::Config.new(deploy_dir, nil).load
|
|
41
41
|
|
|
42
|
+
# Checked before any cluster access: nothing to show without a terminal
|
|
43
|
+
unless Kdep::Dashboard.interactive?
|
|
44
|
+
@ui.error("kdep dashboard needs an interactive terminal (stdin and stdout). " \
|
|
45
|
+
"From a script, use `kdep status` or `kdep log`.")
|
|
46
|
+
exit 1
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
# Validate context before any cluster operation
|
|
43
50
|
begin
|
|
44
51
|
Kdep::ContextGuard.new(config["context"]).validate!
|
|
@@ -73,6 +73,10 @@ module Kdep
|
|
|
73
73
|
exit 1
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
# O Secret extra (preset helm: secret + ingress) é conferido ANTES do helm upgrade.
|
|
77
|
+
Kdep::Renderer.preflight_secrets!(@ui, Kdep::Renderer.new(config, @deploy_dir, env: @env),
|
|
78
|
+
Kdep::Preset.new("helm", @deploy_dir).resources)
|
|
79
|
+
|
|
76
80
|
# Load secrets for --set interpolation
|
|
77
81
|
@secrets = load_secrets(@deploy_dir)
|
|
78
82
|
|
|
@@ -277,6 +281,9 @@ module Kdep
|
|
|
277
281
|
index = next_index
|
|
278
282
|
begin
|
|
279
283
|
content = renderer.render_resource(resource_name)
|
|
284
|
+
rescue Kdep::Renderer::MissingSecretsError => e
|
|
285
|
+
@ui.error("#{resource_name}: #{e.message}")
|
|
286
|
+
exit 1
|
|
280
287
|
rescue => e
|
|
281
288
|
render_errors << "#{resource_name}: #{e.message}"
|
|
282
289
|
next
|
data/lib/kdep/commands/log.rb
CHANGED
|
@@ -3,9 +3,6 @@ require "optparse"
|
|
|
3
3
|
module Kdep
|
|
4
4
|
module Commands
|
|
5
5
|
class Log
|
|
6
|
-
# kubectl refuses to follow more than --max-log-requests streams (default 5)
|
|
7
|
-
MIN_LOG_REQUESTS = 5
|
|
8
|
-
|
|
9
6
|
def self.option_parser
|
|
10
7
|
OptionParser.new do |opts|
|
|
11
8
|
opts.banner = "Usage: kdep log [deploy]"
|
|
@@ -64,39 +61,26 @@ module Kdep
|
|
|
64
61
|
|
|
65
62
|
private
|
|
66
63
|
|
|
67
|
-
# kubectl logs -l streams every pod the selector matches, whatever its
|
|
68
|
-
# phase (Completed leftovers, Pending surge pods), so the stream limit
|
|
69
|
-
# must count all of them -- only the guard looks at Running.
|
|
70
64
|
def count_pods(namespace, app_name)
|
|
71
|
-
|
|
72
|
-
"get", "pods",
|
|
73
|
-
"-n", namespace,
|
|
74
|
-
"-l", "app=#{app_name}"
|
|
75
|
-
)
|
|
76
|
-
pods = data["items"] || []
|
|
65
|
+
pod_count, running = Kdep::PodLogs.count(namespace, app_name)
|
|
77
66
|
|
|
78
|
-
if
|
|
67
|
+
if running.zero?
|
|
79
68
|
@ui.error("No running pods found for #{app_name} in #{namespace}")
|
|
80
69
|
exit 1
|
|
81
70
|
end
|
|
82
71
|
|
|
83
|
-
|
|
72
|
+
pod_count
|
|
84
73
|
end
|
|
85
74
|
|
|
86
75
|
def stream_logs(namespace, app_name, pod_count)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if container
|
|
97
|
-
args << "-c"
|
|
98
|
-
args << container
|
|
99
|
-
end
|
|
76
|
+
args = Kdep::PodLogs.kubectl_args(
|
|
77
|
+
namespace: namespace,
|
|
78
|
+
app_name: app_name,
|
|
79
|
+
pod_count: pod_count,
|
|
80
|
+
tail: @command_options.fetch(:tail, 100),
|
|
81
|
+
follow: @command_options.fetch(:follow, true),
|
|
82
|
+
container: @command_options[:container]
|
|
83
|
+
)
|
|
100
84
|
|
|
101
85
|
io = IO.popen(args, "r")
|
|
102
86
|
|
|
@@ -3,9 +3,6 @@ require "optparse"
|
|
|
3
3
|
module Kdep
|
|
4
4
|
module Commands
|
|
5
5
|
class Restart
|
|
6
|
-
WORKLOAD_KINDS = %w[deployment statefulset daemonset].freeze
|
|
7
|
-
NON_RESTARTABLE_KINDS = %w[job cronjob].freeze
|
|
8
|
-
|
|
9
6
|
def self.option_parser
|
|
10
7
|
OptionParser.new do |opts|
|
|
11
8
|
opts.banner = "Usage: kdep restart [deploy] [--type TYPE]"
|
|
@@ -73,10 +70,7 @@ module Kdep
|
|
|
73
70
|
|
|
74
71
|
private
|
|
75
72
|
|
|
76
|
-
#
|
|
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.
|
|
73
|
+
# Kind comes from the preset's resource list (see Kdep::Workload).
|
|
80
74
|
def determine_resource_type(preset, deploy_dir)
|
|
81
75
|
if @command_options[:type]
|
|
82
76
|
return @command_options[:type]
|
|
@@ -86,14 +80,14 @@ module Kdep
|
|
|
86
80
|
end
|
|
87
81
|
|
|
88
82
|
def resource_type_for_preset(preset, deploy_dir)
|
|
89
|
-
resources = preset_resources(preset, deploy_dir)
|
|
83
|
+
resources = Kdep::Workload.preset_resources(preset, deploy_dir)
|
|
90
84
|
|
|
91
|
-
if (kind =
|
|
85
|
+
if (kind = Kdep::Workload.batch_kind(resources))
|
|
92
86
|
@ui.error("#{kind} workloads cannot be restarted (use --type to override)")
|
|
93
87
|
exit 1
|
|
94
88
|
end
|
|
95
89
|
|
|
96
|
-
found = resources
|
|
90
|
+
found = Kdep::Workload.rollout_kinds(resources)
|
|
97
91
|
|
|
98
92
|
if found.size > 1
|
|
99
93
|
@ui.error("Multiple workloads in preset '#{preset}': #{found.join(', ')}. Use --type to pick one.")
|
|
@@ -106,12 +100,6 @@ module Kdep
|
|
|
106
100
|
found.first || "deployment"
|
|
107
101
|
end
|
|
108
102
|
|
|
109
|
-
def preset_resources(preset, deploy_dir)
|
|
110
|
-
Kdep::Preset.new(preset, deploy_dir).resources
|
|
111
|
-
rescue StandardError
|
|
112
|
-
[]
|
|
113
|
-
end
|
|
114
|
-
|
|
115
103
|
def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
|
|
116
104
|
if deploy_name
|
|
117
105
|
deploy_dir = File.join(kdep_dir, deploy_name)
|
data/lib/kdep/dashboard.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "kdep/dashboard/log_panel"
|
|
|
7
7
|
require "kdep/dashboard/resources_panel"
|
|
8
8
|
require "kdep/dashboard/health_panel"
|
|
9
9
|
require "kdep/kubectl"
|
|
10
|
+
require "kdep/pod_logs"
|
|
10
11
|
|
|
11
12
|
module Kdep
|
|
12
13
|
class Dashboard
|
|
@@ -14,6 +15,13 @@ module Kdep
|
|
|
14
15
|
PANEL_NAMES = { rollout: "Rollout", logs: "Logs", resources: "Resources", health: "Health" }.freeze
|
|
15
16
|
REFRESH_INTERVAL = 0.25
|
|
16
17
|
|
|
18
|
+
# The dashboard reads keys through $stdin.raw and draws on $stdout, so
|
|
19
|
+
# both must be terminals -- a script with a piped or /dev/null stdin
|
|
20
|
+
# still has a TTY stdout and used to crash with Errno::ENOTTY.
|
|
21
|
+
def self.interactive?
|
|
22
|
+
$stdin.tty? && $stdout.tty?
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
def initialize(config)
|
|
18
26
|
@config = config
|
|
19
27
|
@name = config["name"]
|
|
@@ -29,8 +37,8 @@ module Kdep
|
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def run
|
|
32
|
-
unless
|
|
33
|
-
puts "Dashboard skipped: not
|
|
40
|
+
unless self.class.interactive?
|
|
41
|
+
puts "Dashboard skipped: not an interactive terminal"
|
|
34
42
|
return
|
|
35
43
|
end
|
|
36
44
|
|
|
@@ -152,8 +160,9 @@ module Kdep
|
|
|
152
160
|
def start_log_thread
|
|
153
161
|
t = Thread.new do
|
|
154
162
|
begin
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
pod_count, _running = PodLogs.count(@namespace, @name)
|
|
164
|
+
cmd = PodLogs.kubectl_args(namespace: @namespace, app_name: @name, pod_count: pod_count,
|
|
165
|
+
tail: 50, all_containers: true)
|
|
157
166
|
io = IO.popen(cmd, "r", err: [:child, :out])
|
|
158
167
|
@mutex.synchronize { @ios << io }
|
|
159
168
|
while @running && (line = io.gets)
|
data/lib/kdep/kubectl.rb
CHANGED
|
@@ -40,6 +40,12 @@ module Kdep
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Streams kubectl's own progress to the terminal; true when the rollout
|
|
44
|
+
# completed within the timeout.
|
|
45
|
+
def self.rollout_status(kind, name, namespace:, timeout:)
|
|
46
|
+
system("kubectl", "rollout", "status", "#{kind}/#{name}", "-n", namespace, "--timeout=#{timeout}")
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
# Declarative rollout-restart helper used by Kdep::RolloutTracker (Feature F).
|
|
44
50
|
def self.rollout_restart(kind, name, namespace:)
|
|
45
51
|
run("rollout", "restart", "#{kind}/#{name}", "-n", namespace)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "kdep/kubectl"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
# Shared by `kdep log` and the dashboard log panel: both stream every pod
|
|
5
|
+
# behind the app label instead of picking one.
|
|
6
|
+
module PodLogs
|
|
7
|
+
# kubectl refuses to follow more than --max-log-requests streams (default 5)
|
|
8
|
+
MIN_LOG_REQUESTS = 5
|
|
9
|
+
# The count and `kubectl logs -l` are two selector lookups. Right after a
|
|
10
|
+
# bump the ReplicaSet is creating surge pods between them, so a limit equal
|
|
11
|
+
# to the count gets refused -- leave room for a full surge.
|
|
12
|
+
SURGE_HEADROOM = 2
|
|
13
|
+
|
|
14
|
+
# kubectl logs -l streams every pod the selector matches, whatever its
|
|
15
|
+
# phase (Completed leftovers, Pending surge pods), so the stream limit
|
|
16
|
+
# must count all of them. Returns [matching, running].
|
|
17
|
+
def self.count(namespace, app_name)
|
|
18
|
+
data = Kdep::Kubectl.run_json("get", "pods", "-n", namespace, "-l", "app=#{app_name}")
|
|
19
|
+
pods = data["items"] || []
|
|
20
|
+
running = pods.count { |pod| pod.dig("status", "phase") == "Running" }
|
|
21
|
+
[pods.length, running]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# --ignore-errors keeps one pod still in ContainerCreating from ending
|
|
25
|
+
# every other stream mid-rollout.
|
|
26
|
+
def self.kubectl_args(namespace:, app_name:, pod_count:, tail:, follow: true, container: nil, all_containers: false)
|
|
27
|
+
args = ["kubectl", "logs", "-n", namespace, "-l", "app=#{app_name}", "--prefix", "--ignore-errors"]
|
|
28
|
+
args << "--max-log-requests=#{[pod_count * SURGE_HEADROOM, MIN_LOG_REQUESTS].max}"
|
|
29
|
+
args << "-f" if follow
|
|
30
|
+
args << "--tail=#{tail}"
|
|
31
|
+
|
|
32
|
+
if container
|
|
33
|
+
args << "-c"
|
|
34
|
+
args << container
|
|
35
|
+
elsif all_containers
|
|
36
|
+
args << "--all-containers"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
args
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/kdep/renderer.rb
CHANGED
|
@@ -3,6 +3,22 @@ require "yaml"
|
|
|
3
3
|
|
|
4
4
|
module Kdep
|
|
5
5
|
class Renderer
|
|
6
|
+
# O app declara env_from para o Secret que o kdep renderiza, mas não há secrets.yml (ou está vazio):
|
|
7
|
+
# aplicar esse Secret sem `data` apagaria todas as chaves dele no cluster (incidente de 12/11/2026).
|
|
8
|
+
class MissingSecretsError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Preflight comum a bump/apply/helm: roda ANTES de qualquer build, helm upgrade ou apply.
|
|
11
|
+
# Erro -> mensagem + exit 1, nada aplicado. :skip -> aviso (o Secret do cluster fica intocado).
|
|
12
|
+
def self.preflight_secrets!(ui, renderer, resources)
|
|
13
|
+
return unless resources.include?("secret")
|
|
14
|
+
|
|
15
|
+
ui.warn(renderer.secret_skip_message) if renderer.check_secrets! == :skip
|
|
16
|
+
rescue MissingSecretsError => e
|
|
17
|
+
ui.error(e.message)
|
|
18
|
+
ui.error("nada foi aplicado")
|
|
19
|
+
exit 1
|
|
20
|
+
end
|
|
21
|
+
|
|
6
22
|
# Convention: Infisical project slug = K8s namespace stripped of -stage/-dev
|
|
7
23
|
# suffix; envSlug = stage/dev/prod derived from the same suffix.
|
|
8
24
|
NS_ENV_SUFFIXES = { "-stage" => "stage", "-staging" => "stage", "-dev" => "dev" }.freeze
|
|
@@ -39,6 +55,7 @@ module Kdep
|
|
|
39
55
|
return nil if secret_keys_for_env.empty?
|
|
40
56
|
effective_config = @config.merge("infisical" => infisical_render_context)
|
|
41
57
|
when "secret"
|
|
58
|
+
return nil if check_secrets! == :skip
|
|
42
59
|
secrets = legacy_load_secrets
|
|
43
60
|
end
|
|
44
61
|
|
|
@@ -179,5 +196,59 @@ module Kdep
|
|
|
179
196
|
content = File.read(secrets_path)
|
|
180
197
|
YAML.safe_load(content) || {}
|
|
181
198
|
end
|
|
199
|
+
|
|
200
|
+
public
|
|
201
|
+
|
|
202
|
+
# Decide o que fazer com o Secret nativo (kdep/<deploy>/secrets.yml). Nunca aplica um Secret que apague chaves:
|
|
203
|
+
# - secrets.yml com chaves, todas com valor -> :render
|
|
204
|
+
# - secrets.yml ausente ou vazio, env_from padrão -> :skip (não renderiza; o Secret do cluster fica intocado)
|
|
205
|
+
# - secrets.yml ausente ou vazio, env_from EXPLÍCITO para este Secret -> MissingSecretsError
|
|
206
|
+
# - alguma chave com valor nil ou "" -> MissingSecretsError (apagaria o valor no cluster)
|
|
207
|
+
# Incidente de 12/11/2026: o secret-mcp-leadfy-bi foi aplicado sem data e perdeu todas as chaves.
|
|
208
|
+
def check_secrets!
|
|
209
|
+
return :render if env_spec
|
|
210
|
+
|
|
211
|
+
secrets = legacy_load_secrets
|
|
212
|
+
unless secrets.is_a?(Hash)
|
|
213
|
+
raise MissingSecretsError, "#{secrets_path} não é um mapa CHAVE: valor"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
if secrets.empty?
|
|
217
|
+
return :skip unless explicit_secret_ref?
|
|
218
|
+
|
|
219
|
+
state = File.exist?(secrets_path) ? "está vazio" : "não existe"
|
|
220
|
+
raise MissingSecretsError,
|
|
221
|
+
"#{secrets_path} #{state}, mas o app.yml tem env_from secret/#{secret_name}: " \
|
|
222
|
+
"aplicar o Secret sem data apagaria todas as chaves de #{secret_name} no cluster. " \
|
|
223
|
+
"Crie o secrets.yml com as chaves (ou tire o secret do env_from) antes de render/apply/bump."
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
vazias = secrets.select { |_k, v| v.nil? || v.to_s.empty? }.keys
|
|
227
|
+
unless vazias.empty?
|
|
228
|
+
raise MissingSecretsError,
|
|
229
|
+
"#{secrets_path}: chave(s) sem valor (#{vazias.join(", ")}). Aplicar assim apagaria o valor " \
|
|
230
|
+
"delas em #{secret_name} no cluster. Preencha ou remova antes de render/apply/bump."
|
|
231
|
+
end
|
|
232
|
+
:render
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def secret_skip_message
|
|
236
|
+
"#{secrets_path} não existe (ou está vazio): o Secret #{secret_name} não é renderizado nem aplicado; " \
|
|
237
|
+
"o que estiver no cluster fica intocado (se ele não existir, o pod falha ao subir)."
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def secret_name
|
|
243
|
+
@config["secret_name"] || "secret-#{@config["name"]}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def secrets_path
|
|
247
|
+
File.join(@deploy_dir, "secrets.yml")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def explicit_secret_ref?
|
|
251
|
+
Array(@config["env_from"]).map(&:to_s).include?("secret/#{secret_name}")
|
|
252
|
+
end
|
|
182
253
|
end
|
|
183
254
|
end
|
data/lib/kdep/version.rb
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "kdep/preset"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
# The preset file lists the resources kdep applies, so the workload kind is
|
|
5
|
+
# declared there -- deriving it from the preset *name* got every
|
|
6
|
+
# non-Deployment target wrong (daemonset, statefulset, custom). Reading the
|
|
7
|
+
# list also handles `custom`, whose resources live in the deploy dir.
|
|
8
|
+
# Shared by `kdep restart` and `kdep bump --wait`.
|
|
9
|
+
module Workload
|
|
10
|
+
KINDS = %w[deployment statefulset daemonset].freeze
|
|
11
|
+
BATCH_KINDS = %w[job cronjob].freeze
|
|
12
|
+
|
|
13
|
+
def self.preset_resources(preset, deploy_dir)
|
|
14
|
+
Kdep::Preset.new(preset, deploy_dir).resources
|
|
15
|
+
rescue StandardError
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Workloads that have a rollout (restart / rollout status)
|
|
20
|
+
def self.rollout_kinds(resources)
|
|
21
|
+
resources & KINDS
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The workload's metadata.name, resolved exactly as the templates do.
|
|
25
|
+
def self.name_for(kind, config)
|
|
26
|
+
case kind
|
|
27
|
+
when "statefulset" then config["statefulset_name"] || config["deployment_name"] || config["name"]
|
|
28
|
+
when "daemonset" then config["daemonset_name"] || config["name"]
|
|
29
|
+
else config["deployment_name"] || config["name"]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.batch_kind(resources)
|
|
34
|
+
resources.find { |r| BATCH_KINDS.include?(r) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/kdep.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "kdep/yaml_compat"
|
|
|
5
5
|
require "kdep/defaults"
|
|
6
6
|
require "kdep/config"
|
|
7
7
|
require "kdep/preset"
|
|
8
|
+
require "kdep/workload"
|
|
8
9
|
require "kdep/discovery"
|
|
9
10
|
require "kdep/template_context"
|
|
10
11
|
require "kdep/renderer"
|
|
@@ -12,6 +13,7 @@ require "kdep/validator"
|
|
|
12
13
|
require "kdep/writer"
|
|
13
14
|
require "kdep/old_format"
|
|
14
15
|
require "kdep/kubectl"
|
|
16
|
+
require "kdep/pod_logs"
|
|
15
17
|
require "kdep/context_guard"
|
|
16
18
|
require "kdep/cluster_health"
|
|
17
19
|
require "kdep/docker"
|
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
|
+
version: 0.6.0
|
|
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-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: envspec
|
|
@@ -131,6 +131,7 @@ files:
|
|
|
131
131
|
- lib/kdep/kubectl.rb
|
|
132
132
|
- lib/kdep/old_format.rb
|
|
133
133
|
- lib/kdep/path_resolver.rb
|
|
134
|
+
- lib/kdep/pod_logs.rb
|
|
134
135
|
- lib/kdep/preset.rb
|
|
135
136
|
- lib/kdep/registry.rb
|
|
136
137
|
- lib/kdep/renderer.rb
|
|
@@ -143,6 +144,7 @@ files:
|
|
|
143
144
|
- lib/kdep/validator.rb
|
|
144
145
|
- lib/kdep/version.rb
|
|
145
146
|
- lib/kdep/version_tagger.rb
|
|
147
|
+
- lib/kdep/workload.rb
|
|
146
148
|
- lib/kdep/writer.rb
|
|
147
149
|
- lib/kdep/yaml_compat.rb
|
|
148
150
|
- lib/kdep/yaml_duplicate_keys.rb
|