kdep 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ed0d8de8dfb58cc372615340a068aecb3f8092aecafdbc6d1c6480c14477d57
4
- data.tar.gz: 8dde50b29b917321144fcca1d0cb83cff8b66a702442c5bf19375ae527f4cf6e
3
+ metadata.gz: 6e1f1a30cdbd53c887f8cb3596553d8cadfd8c40a69654e233effb307d405bde
4
+ data.tar.gz: de0c60773483b9b9203463d807cd61af12a4aacdee78f71c834522ef38538116
5
5
  SHA512:
6
- metadata.gz: 3669fafcc6315fcdb7af39bb0b31cb7314a953b3185988523af63e1d9ab1fb60a458ba141dc22e3735e00de5df8a821b862b657aaae68d36e8326e0a0f304f2b
7
- data.tar.gz: 7cbb7445b6909b46e74f47245c88df5fa8d7d93617a949b14059aef3f50c2981941ed129d70f013dede5e2789ad363e55d8768698129059c412c03d631851b81
6
+ metadata.gz: 057d0b80e63baaae39146d5045ff33a1a152dcea0809aa27e59bf2e9e1bb08db846ef8146df7ce512b679612c05abf9777abd8c377156bee9cc2615d23000190
7
+ data.tar.gz: 32378759cf7fec1f2f81f3d4896059fba2383e2c095e500adafeee43be6270a756a1d787ec89a3395ba66127de6e22afbe5dbd1d3d7d0d4de134bf498556f58e
@@ -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
@@ -198,8 +213,13 @@ module Kdep
198
213
  # Save new tag to state.yml
199
214
  Kdep::State.write(deploy_dir, tag: next_version)
200
215
 
201
- # Launch TUI dashboard for live monitoring
202
- unless @command_options[:"no-dashboard"]
216
+ # Follow the rollout: --wait blocks on it (scripts/CI), otherwise
217
+ # the TUI dashboard when there is a terminal to draw it on.
218
+ if @command_options[:wait]
219
+ wait_for_rollout(config, deploy_dir, namespace)
220
+ elsif @command_options[:"no-dashboard"]
221
+ # explicitly opted out
222
+ elsif Kdep::Dashboard.interactive?
203
223
  dashboard = Kdep::Dashboard.new(
204
224
  "name" => config["name"],
205
225
  "namespace" => namespace,
@@ -207,6 +227,8 @@ module Kdep
207
227
  "image" => image
208
228
  )
209
229
  dashboard.run
230
+ else
231
+ @ui.warn("rollout not awaited -- pass --wait to block until it completes (exit 1 if it fails)")
210
232
  end
211
233
  end
212
234
  end
@@ -222,6 +244,29 @@ module Kdep
222
244
 
223
245
  private
224
246
 
247
+ def wait_for_rollout(config, deploy_dir, namespace)
248
+ deploy = File.basename(deploy_dir)
249
+ resources = Kdep::Workload.preset_resources(config["preset"], deploy_dir)
250
+ kinds = Kdep::Workload.rollout_kinds(resources)
251
+
252
+ if kinds.empty?
253
+ what = Kdep::Workload.batch_kind(resources) || "no deployment/statefulset/daemonset"
254
+ @ui.warn("--wait: preset '#{config["preset"]}' declares #{what}; no rollout to wait for")
255
+ return
256
+ end
257
+
258
+ timeout = @command_options[:timeout] || DEFAULT_ROLLOUT_TIMEOUT
259
+ kinds.each do |kind|
260
+ name = Kdep::Workload.name_for(kind, config)
261
+ @ui.info("waiting for #{kind}/#{name} rollout (timeout #{timeout})...")
262
+ next if Kdep::Kubectl.rollout_status(kind, name, namespace: namespace, timeout: timeout)
263
+
264
+ @ui.error("rollout of #{kind}/#{name} did not complete within #{timeout}. " \
265
+ "Check `kdep status #{deploy}` and `kdep log #{deploy}`.")
266
+ exit 1
267
+ end
268
+ end
269
+
225
270
  # Feature E: helm-preset bump = helm-install pipeline. Preflight
226
271
  # already ran; context guard runs inside HelmInstall; rollout flush
227
272
  # and configmap overlays are internal to it.
@@ -268,6 +313,8 @@ module Kdep
268
313
  Kdep::Kubectl.apply(file_path, namespace: namespace)
269
314
  @ui.info("applied: #{File.basename(file_path)}")
270
315
  end
316
+
317
+ wait_for_rollout(config, deploy_dir, namespace) if @command_options[:wait]
271
318
  end
272
319
  end
273
320
 
@@ -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!
@@ -7,7 +7,7 @@ module Kdep
7
7
  OptionParser.new do |opts|
8
8
  opts.banner = "Usage: kdep log [deploy]"
9
9
  opts.separator ""
10
- opts.separator "Streams pod logs with error highlighting."
10
+ opts.separator "Streams logs from all running pods (prefixed by pod) with error highlighting."
11
11
  opts.separator ""
12
12
  opts.on("-f", "--[no-]follow", "Follow log output (default: true)")
13
13
  opts.on("-c", "--container=NAME", "Container name for multi-container pods")
@@ -50,47 +50,37 @@ module Kdep
50
50
  exit 1
51
51
  end
52
52
 
53
- # Find running pod
53
+ # Count matching pods (fails fast when none is running)
54
54
  namespace = config["namespace"]
55
55
  app_name = config["name"]
56
- pod_name = find_pod(namespace, app_name)
56
+ pod_count = count_pods(namespace, app_name)
57
57
 
58
- # Stream logs
59
- stream_logs(pod_name, namespace)
58
+ # Stream logs from every pod matching the app label
59
+ stream_logs(namespace, app_name, pod_count)
60
60
  end
61
61
 
62
62
  private
63
63
 
64
- def find_pod(namespace, app_name)
65
- data = Kdep::Kubectl.run_json(
66
- "get", "pods",
67
- "-n", namespace,
68
- "-l", "app=#{app_name}",
69
- "--field-selector", "status.phase=Running"
70
- )
71
- pods = data["items"]
64
+ def count_pods(namespace, app_name)
65
+ pod_count, running = Kdep::PodLogs.count(namespace, app_name)
72
66
 
73
- if pods.nil? || pods.empty?
67
+ if running.zero?
74
68
  @ui.error("No running pods found for #{app_name} in #{namespace}")
75
69
  exit 1
76
70
  end
77
71
 
78
- pods[0]["metadata"]["name"]
72
+ pod_count
79
73
  end
80
74
 
81
- def stream_logs(pod_name, namespace)
82
- follow = @command_options.fetch(:follow, true)
83
- tail = @command_options.fetch(:tail, 100)
84
- container = @command_options[:container]
85
-
86
- args = ["kubectl", "logs", pod_name, "-n", namespace]
87
- args << "-f" if follow
88
- args << "--tail=#{tail}"
89
-
90
- if container
91
- args << "-c"
92
- args << container
93
- end
75
+ def stream_logs(namespace, app_name, pod_count)
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
+ )
94
84
 
95
85
  io = IO.popen(args, "r")
96
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
- # 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.
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 = resources.find { |r| NON_RESTARTABLE_KINDS.include?(r) })
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 & WORKLOAD_KINDS
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)
@@ -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 $stdout.tty?
33
- puts "Dashboard skipped: not a TTY"
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
- cmd = ["kubectl", "logs", "-n", @namespace, "-l", "app=#{@name}",
156
- "-f", "--tail=50", "--all-containers", "--prefix"]
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/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kdep
2
- VERSION = "0.4.8"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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.8
4
+ version: 0.5.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-08-15 00:00:00.000000000 Z
11
+ date: 2026-10-10 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
@@ -189,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
191
  - !ruby/object:Gem::Version
190
192
  version: '0'
191
193
  requirements: []
192
- rubygems_version: 3.2.32
194
+ rubygems_version: 3.5.19
193
195
  signing_key:
194
196
  specification_version: 4
195
197
  summary: Kubernetes deployment CLI