kdep 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/exe/kdep +3 -0
- data/lib/kdep/cli.rb +72 -0
- data/lib/kdep/cluster_health.rb +44 -0
- data/lib/kdep/commands/apply.rb +147 -0
- data/lib/kdep/commands/build.rb +103 -0
- data/lib/kdep/commands/bump.rb +190 -0
- data/lib/kdep/commands/diff.rb +133 -0
- data/lib/kdep/commands/eject.rb +97 -0
- data/lib/kdep/commands/init.rb +99 -0
- data/lib/kdep/commands/log.rb +145 -0
- data/lib/kdep/commands/push.rb +94 -0
- data/lib/kdep/commands/render.rb +130 -0
- data/lib/kdep/commands/restart.rb +109 -0
- data/lib/kdep/commands/scale.rb +136 -0
- data/lib/kdep/commands/secrets.rb +223 -0
- data/lib/kdep/commands/sh.rb +117 -0
- data/lib/kdep/commands/status.rb +187 -0
- data/lib/kdep/config.rb +69 -0
- data/lib/kdep/context_guard.rb +23 -0
- data/lib/kdep/dashboard/health_panel.rb +53 -0
- data/lib/kdep/dashboard/layout.rb +40 -0
- data/lib/kdep/dashboard/log_panel.rb +54 -0
- data/lib/kdep/dashboard/panel.rb +135 -0
- data/lib/kdep/dashboard/resources_panel.rb +91 -0
- data/lib/kdep/dashboard/rollout_panel.rb +75 -0
- data/lib/kdep/dashboard/screen.rb +29 -0
- data/lib/kdep/dashboard.rb +258 -0
- data/lib/kdep/defaults.rb +21 -0
- data/lib/kdep/discovery.rb +29 -0
- data/lib/kdep/docker.rb +29 -0
- data/lib/kdep/kubectl.rb +41 -0
- data/lib/kdep/preset.rb +33 -0
- data/lib/kdep/registry.rb +94 -0
- data/lib/kdep/renderer.rb +53 -0
- data/lib/kdep/template_context.rb +22 -0
- data/lib/kdep/ui.rb +48 -0
- data/lib/kdep/validator.rb +73 -0
- data/lib/kdep/version.rb +3 -0
- data/lib/kdep/version_tagger.rb +27 -0
- data/lib/kdep/writer.rb +26 -0
- data/lib/kdep.rb +49 -0
- data/templates/init/app.yml.erb +49 -0
- data/templates/init/gitignore +3 -0
- data/templates/init/secrets.yml +7 -0
- data/templates/presets/cronjob +4 -0
- data/templates/presets/job +4 -0
- data/templates/presets/web +7 -0
- data/templates/presets/worker +4 -0
- data/templates/resources/configmap.yml.erb +7 -0
- data/templates/resources/cronjob.yml.erb +42 -0
- data/templates/resources/deployment.yml.erb +71 -0
- data/templates/resources/ingress.yml.erb +47 -0
- data/templates/resources/job.yml.erb +35 -0
- data/templates/resources/secret.yml.erb +15 -0
- data/templates/resources/service.yml.erb +13 -0
- metadata +142 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
class Dashboard
|
|
3
|
+
class RolloutPanel < Panel
|
|
4
|
+
def update_from_deployment(deployment_json)
|
|
5
|
+
spec = deployment_json["spec"] || {}
|
|
6
|
+
status = deployment_json["status"] || {}
|
|
7
|
+
|
|
8
|
+
desired = spec["replicas"] || 1
|
|
9
|
+
updated = status["updatedReplicas"] || 0
|
|
10
|
+
available = status["availableReplicas"] || 0
|
|
11
|
+
ready = status["readyReplicas"] || 0
|
|
12
|
+
|
|
13
|
+
percent = desired > 0 ? (updated * 100 / desired) : 0
|
|
14
|
+
|
|
15
|
+
conditions = status["conditions"] || []
|
|
16
|
+
progressing = conditions.find { |c| c["type"] == "Progressing" }
|
|
17
|
+
failed = progressing && progressing["status"] == "False"
|
|
18
|
+
message = progressing ? progressing["message"] : nil
|
|
19
|
+
|
|
20
|
+
@rollout = {
|
|
21
|
+
desired: desired,
|
|
22
|
+
updated: updated,
|
|
23
|
+
available: available,
|
|
24
|
+
ready: ready,
|
|
25
|
+
percent: percent,
|
|
26
|
+
failed: failed,
|
|
27
|
+
message: message,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
build_lines
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def build_lines
|
|
36
|
+
return unless @rollout
|
|
37
|
+
|
|
38
|
+
content = []
|
|
39
|
+
percent = @rollout[:percent]
|
|
40
|
+
bar_width = [rect.width - 20, 10].max
|
|
41
|
+
bar = render_progress_bar(percent, bar_width)
|
|
42
|
+
|
|
43
|
+
if @rollout[:failed]
|
|
44
|
+
content << "\e[31mRollout: #{percent}% #{bar}\e[0m"
|
|
45
|
+
elsif percent >= 100
|
|
46
|
+
content << "\e[32mRollout: 100% #{bar}\e[0m"
|
|
47
|
+
content << "\e[32mRollout complete\e[0m"
|
|
48
|
+
else
|
|
49
|
+
content << "Rollout: #{percent}% #{bar}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
d = @rollout[:desired]
|
|
53
|
+
content << "Pods: #{@rollout[:updated]}/#{d} updated, #{@rollout[:available]}/#{d} available, #{@rollout[:ready]}/#{d} ready"
|
|
54
|
+
|
|
55
|
+
if @rollout[:failed] && @rollout[:message]
|
|
56
|
+
content << "\e[31m#{@rollout[:message]}\e[0m"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
update(content)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_progress_bar(percent, width)
|
|
63
|
+
filled = (width * percent / 100.0).to_i
|
|
64
|
+
bar = "=" * [filled, 0].max
|
|
65
|
+
if filled < width && filled > 0
|
|
66
|
+
bar += ">"
|
|
67
|
+
space = " " * [width - filled - 1, 0].max
|
|
68
|
+
else
|
|
69
|
+
space = " " * [width - filled, 0].max
|
|
70
|
+
end
|
|
71
|
+
"[#{bar}#{space}] #{percent}%"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
class Dashboard
|
|
3
|
+
module Screen
|
|
4
|
+
ENTER_ALT = "\e[?1049h".freeze
|
|
5
|
+
EXIT_ALT = "\e[?1049l".freeze
|
|
6
|
+
HIDE_CURSOR = "\e[?25l".freeze
|
|
7
|
+
SHOW_CURSOR = "\e[?25h".freeze
|
|
8
|
+
CLEAR_SCREEN = "\e[2J".freeze
|
|
9
|
+
HOME = "\e[H".freeze
|
|
10
|
+
CLEAR_LINE_SEQ = "\e[2K".freeze
|
|
11
|
+
|
|
12
|
+
def self.enter_sequence
|
|
13
|
+
ENTER_ALT + HIDE_CURSOR + CLEAR_SCREEN + HOME
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.exit_sequence
|
|
17
|
+
SHOW_CURSOR + EXIT_ALT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.move_to(row, col)
|
|
21
|
+
"\e[#{row};#{col}H"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.clear_line
|
|
25
|
+
CLEAR_LINE_SEQ
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
require "io/console"
|
|
2
|
+
require "kdep/dashboard/screen"
|
|
3
|
+
require "kdep/dashboard/layout"
|
|
4
|
+
require "kdep/dashboard/panel"
|
|
5
|
+
require "kdep/dashboard/rollout_panel"
|
|
6
|
+
require "kdep/dashboard/log_panel"
|
|
7
|
+
require "kdep/dashboard/resources_panel"
|
|
8
|
+
require "kdep/dashboard/health_panel"
|
|
9
|
+
require "kdep/kubectl"
|
|
10
|
+
|
|
11
|
+
module Kdep
|
|
12
|
+
class Dashboard
|
|
13
|
+
PANEL_ORDER = [:rollout, :logs, :resources, :health].freeze
|
|
14
|
+
PANEL_NAMES = { rollout: "Rollout", logs: "Logs", resources: "Resources", health: "Health" }.freeze
|
|
15
|
+
REFRESH_INTERVAL = 0.25
|
|
16
|
+
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
@name = config["name"]
|
|
20
|
+
@namespace = config["namespace"] || "default"
|
|
21
|
+
@deployment_name = config["deployment_name"] || @name
|
|
22
|
+
@panels = {}
|
|
23
|
+
@active_panel = :rollout
|
|
24
|
+
@quit = false
|
|
25
|
+
@running = false
|
|
26
|
+
@threads = []
|
|
27
|
+
@ios = []
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
unless $stdout.tty?
|
|
33
|
+
puts "Dashboard skipped: not a TTY"
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
rows, cols = IO.console.winsize
|
|
38
|
+
return if init_panels(rows, cols).nil?
|
|
39
|
+
|
|
40
|
+
$stdout.print Screen.enter_sequence
|
|
41
|
+
$stdout.flush
|
|
42
|
+
@running = true
|
|
43
|
+
@quit = false
|
|
44
|
+
|
|
45
|
+
setup_signal_handlers
|
|
46
|
+
start_log_thread
|
|
47
|
+
start_rollout_thread
|
|
48
|
+
start_metrics_thread
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
$stdin.raw do |raw_stdin|
|
|
52
|
+
until @quit
|
|
53
|
+
if IO.select([raw_stdin], nil, nil, 0)
|
|
54
|
+
key = raw_stdin.getc
|
|
55
|
+
handle_key(key) if key
|
|
56
|
+
end
|
|
57
|
+
break if @quit
|
|
58
|
+
render_all_panels($stdout, rows, cols)
|
|
59
|
+
sleep REFRESH_INTERVAL
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
ensure
|
|
63
|
+
stop_data_threads
|
|
64
|
+
$stdout.print Screen.exit_sequence
|
|
65
|
+
$stdout.flush
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def init_panels(rows, cols)
|
|
72
|
+
layout = Layout.new(rows: rows, cols: cols)
|
|
73
|
+
rects = layout.panels
|
|
74
|
+
return nil if rects.nil?
|
|
75
|
+
|
|
76
|
+
@panels = {
|
|
77
|
+
rollout: RolloutPanel.new(title: "Rollout", rect: rects[:rollout]),
|
|
78
|
+
logs: LogPanel.new(title: "Logs", rect: rects[:logs]),
|
|
79
|
+
resources: ResourcesPanel.new(title: "Resources", rect: rects[:resources]),
|
|
80
|
+
health: HealthPanel.new(title: "Health", rect: rects[:health]),
|
|
81
|
+
}
|
|
82
|
+
@active_panel = :rollout
|
|
83
|
+
@panels
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_key(key)
|
|
87
|
+
case key
|
|
88
|
+
when "q"
|
|
89
|
+
@quit = true
|
|
90
|
+
when "\x03" # Ctrl+C
|
|
91
|
+
@quit = true
|
|
92
|
+
when "\t"
|
|
93
|
+
idx = PANEL_ORDER.index(@active_panel) || 0
|
|
94
|
+
@active_panel = PANEL_ORDER[(idx + 1) % PANEL_ORDER.length]
|
|
95
|
+
when "j"
|
|
96
|
+
@mutex.synchronize { @panels[@active_panel].scroll_down }
|
|
97
|
+
when "k"
|
|
98
|
+
@mutex.synchronize { @panels[@active_panel].scroll_up }
|
|
99
|
+
when "1"
|
|
100
|
+
@active_panel = :rollout
|
|
101
|
+
when "2"
|
|
102
|
+
@active_panel = :logs
|
|
103
|
+
when "3"
|
|
104
|
+
@active_panel = :resources
|
|
105
|
+
when "4"
|
|
106
|
+
@active_panel = :health
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def setup_signal_handlers
|
|
111
|
+
Signal.trap("INT") { @quit = true }
|
|
112
|
+
Signal.trap("TERM") { @quit = true }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_all_panels(io, rows, cols)
|
|
116
|
+
buf = ""
|
|
117
|
+
|
|
118
|
+
# Header
|
|
119
|
+
header = "kdep dashboard -- #{@name} (ns: #{@namespace}) -- q:quit tab:switch j/k:scroll"
|
|
120
|
+
if header.length > cols
|
|
121
|
+
header = header[0, cols]
|
|
122
|
+
end
|
|
123
|
+
buf << Screen.move_to(1, 1) << Screen.clear_line << header
|
|
124
|
+
|
|
125
|
+
# Panels
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
@panels.each do |name, panel|
|
|
128
|
+
rect = panel.rect
|
|
129
|
+
lines = panel.render
|
|
130
|
+
|
|
131
|
+
# Highlight active panel border
|
|
132
|
+
lines.each_with_index do |line, i|
|
|
133
|
+
buf << Screen.move_to(rect.top + i + 1, rect.left + 1) << line
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Status bar
|
|
139
|
+
active_name = PANEL_NAMES[@active_panel] || @active_panel.to_s
|
|
140
|
+
status = " Active: #{active_name} | Refreshing..."
|
|
141
|
+
if status.length > cols
|
|
142
|
+
status = status[0, cols]
|
|
143
|
+
end
|
|
144
|
+
buf << Screen.move_to(rows, 1) << Screen.clear_line << status
|
|
145
|
+
|
|
146
|
+
io.print buf
|
|
147
|
+
io.flush
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# --- Data threads ---
|
|
151
|
+
|
|
152
|
+
def start_log_thread
|
|
153
|
+
t = Thread.new do
|
|
154
|
+
begin
|
|
155
|
+
cmd = ["kubectl", "logs", "-n", @namespace, "-l", "app=#{@name}",
|
|
156
|
+
"-f", "--tail=50", "--all-containers", "--prefix"]
|
|
157
|
+
io = IO.popen(cmd, "r", err: [:child, :out])
|
|
158
|
+
@mutex.synchronize { @ios << io }
|
|
159
|
+
while @running && (line = io.gets)
|
|
160
|
+
@mutex.synchronize { @panels[:logs].add_line(line.chomp) }
|
|
161
|
+
end
|
|
162
|
+
rescue => e
|
|
163
|
+
@mutex.synchronize { @panels[:logs].add_line("Log stream error: #{e.message}") } if @running
|
|
164
|
+
ensure
|
|
165
|
+
io.close if io && !io.closed?
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
@threads << t
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def start_rollout_thread
|
|
172
|
+
t = Thread.new do
|
|
173
|
+
interval = 2
|
|
174
|
+
while @running
|
|
175
|
+
begin
|
|
176
|
+
data = Kdep::Kubectl.run_json("get", "deployment", @deployment_name, "-n", @namespace)
|
|
177
|
+
@mutex.synchronize { @panels[:rollout].update_from_deployment(data) }
|
|
178
|
+
interval = 2
|
|
179
|
+
rescue => e
|
|
180
|
+
@mutex.synchronize { @panels[:rollout].add_line("Error: #{e.message}") } if @running
|
|
181
|
+
interval = 5
|
|
182
|
+
end
|
|
183
|
+
sleep_interruptible(interval)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
@threads << t
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def start_metrics_thread
|
|
190
|
+
t = Thread.new do
|
|
191
|
+
while @running
|
|
192
|
+
begin
|
|
193
|
+
# Pod metrics
|
|
194
|
+
pod_output = Kdep::Kubectl.run("top", "pods", "-n", @namespace, "-l", "app=#{@name}", "--no-headers")
|
|
195
|
+
pod_metrics = parse_pod_metrics(pod_output)
|
|
196
|
+
@mutex.synchronize { @panels[:resources].update(pod_metrics) }
|
|
197
|
+
rescue => e
|
|
198
|
+
@mutex.synchronize { @panels[:resources].add_line("Error: #{e.message}") } if @running
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
# Node metrics
|
|
203
|
+
node_output = Kdep::Kubectl.run("top", "nodes", "--no-headers")
|
|
204
|
+
node_data = parse_node_metrics(node_output)
|
|
205
|
+
@mutex.synchronize { @panels[:health].update(node_data) }
|
|
206
|
+
rescue => e
|
|
207
|
+
@mutex.synchronize { @panels[:health].add_line("Error: #{e.message}") } if @running
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
sleep_interruptible(5)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
@threads << t
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def sleep_interruptible(seconds)
|
|
217
|
+
elapsed = 0
|
|
218
|
+
while elapsed < seconds && @running
|
|
219
|
+
sleep 0.5
|
|
220
|
+
elapsed += 0.5
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def stop_data_threads
|
|
225
|
+
@running = false
|
|
226
|
+
|
|
227
|
+
@threads.each do |t|
|
|
228
|
+
t.join(2)
|
|
229
|
+
t.kill if t.alive?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
@ios.each do |io|
|
|
233
|
+
io.close unless io.closed?
|
|
234
|
+
rescue
|
|
235
|
+
# ignore close errors
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
@threads.clear
|
|
239
|
+
@ios.clear
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def parse_pod_metrics(output)
|
|
243
|
+
output.lines.map do |line|
|
|
244
|
+
parts = line.strip.split(/\s+/)
|
|
245
|
+
next if parts.length < 3
|
|
246
|
+
{ name: parts[0], cpu: parts[1], memory: parts[2] }
|
|
247
|
+
end.compact
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def parse_node_metrics(output)
|
|
251
|
+
output.lines.map do |line|
|
|
252
|
+
parts = line.strip.split(/\s+/)
|
|
253
|
+
next if parts.length < 5
|
|
254
|
+
{ name: parts[0], cpu_percent: parts[2].to_s.sub(/%$/, ""), mem_percent: parts[4].to_s.sub(/%$/, "") }
|
|
255
|
+
end.compact
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
module Defaults
|
|
3
|
+
BASE_DEFAULTS = {
|
|
4
|
+
"port" => 8080,
|
|
5
|
+
"replicas" => 1,
|
|
6
|
+
}.freeze
|
|
7
|
+
|
|
8
|
+
PRESET_OVERRIDES = {
|
|
9
|
+
"web" => { "replicas" => 3 },
|
|
10
|
+
"worker" => {},
|
|
11
|
+
"job" => {},
|
|
12
|
+
"cronjob" => {},
|
|
13
|
+
"custom" => {},
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def self.for_preset(preset_name)
|
|
17
|
+
overrides = PRESET_OVERRIDES.fetch(preset_name, {})
|
|
18
|
+
BASE_DEFAULTS.merge(overrides)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
class Discovery
|
|
3
|
+
def initialize(start_dir = Dir.pwd)
|
|
4
|
+
@start_dir = start_dir
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def find_kdep_dir
|
|
8
|
+
dir = @start_dir
|
|
9
|
+
loop do
|
|
10
|
+
candidate = File.join(dir, "kdep")
|
|
11
|
+
return candidate if File.directory?(candidate)
|
|
12
|
+
parent = File.dirname(dir)
|
|
13
|
+
break if parent == dir # reached filesystem root
|
|
14
|
+
dir = parent
|
|
15
|
+
end
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_deploys
|
|
20
|
+
kdep_dir = find_kdep_dir
|
|
21
|
+
return [] unless kdep_dir
|
|
22
|
+
|
|
23
|
+
Dir.entries(kdep_dir)
|
|
24
|
+
.select { |e| File.directory?(File.join(kdep_dir, e)) }
|
|
25
|
+
.reject { |e| e.start_with?(".") }
|
|
26
|
+
.sort
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/kdep/docker.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
module Docker
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
def self.build(tag:, context_dir:, dockerfile: nil, target: nil, platform: nil)
|
|
8
|
+
args = ["docker", "build", "-t", tag]
|
|
9
|
+
args.push("--file", dockerfile) if dockerfile
|
|
10
|
+
args.push("--target", target) if target
|
|
11
|
+
args.push("--platform", platform) if platform
|
|
12
|
+
args.push(context_dir)
|
|
13
|
+
|
|
14
|
+
stdout, stderr, status = Open3.capture3(*args)
|
|
15
|
+
unless status.success?
|
|
16
|
+
raise Error, "docker build failed: #{stderr.strip}"
|
|
17
|
+
end
|
|
18
|
+
stdout
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.push(tag)
|
|
22
|
+
stdout, stderr, status = Open3.capture3("docker", "push", tag)
|
|
23
|
+
unless status.success?
|
|
24
|
+
raise Error, "docker push failed: #{stderr.strip}"
|
|
25
|
+
end
|
|
26
|
+
stdout
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/kdep/kubectl.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module Kdep
|
|
5
|
+
module Kubectl
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
def self.run(*args)
|
|
9
|
+
stdout, stderr, status = Open3.capture3("kubectl", *args)
|
|
10
|
+
unless status.success?
|
|
11
|
+
raise Error, "kubectl #{args.first} failed: #{stderr.strip}"
|
|
12
|
+
end
|
|
13
|
+
stdout
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.run_json(*args)
|
|
17
|
+
output = run(*args, "-o", "json")
|
|
18
|
+
JSON.parse(output)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.current_context
|
|
22
|
+
run("config", "current-context").strip
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.apply(file_path, namespace:)
|
|
26
|
+
run("apply", "-f", file_path, "-n", namespace)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.diff(file_path)
|
|
30
|
+
stdout, stderr, status = Open3.capture3("kubectl", "diff", "-f", file_path)
|
|
31
|
+
case status.exitstatus
|
|
32
|
+
when 0
|
|
33
|
+
nil
|
|
34
|
+
when 1
|
|
35
|
+
stdout
|
|
36
|
+
else
|
|
37
|
+
raise Error, "kubectl diff failed (exit #{status.exitstatus}): #{stderr.strip}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/kdep/preset.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
class Preset
|
|
3
|
+
BUILT_IN = %w[web worker job cronjob custom].freeze
|
|
4
|
+
|
|
5
|
+
def initialize(preset_name, deploy_dir)
|
|
6
|
+
@preset_name = preset_name
|
|
7
|
+
@deploy_dir = deploy_dir
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resources
|
|
11
|
+
path = resolve_preset_path
|
|
12
|
+
raise "Preset not found: #{@preset_name}" unless path
|
|
13
|
+
|
|
14
|
+
File.readlines(path).map(&:strip).reject do |line|
|
|
15
|
+
line.empty? || line.start_with?("#")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def resolve_preset_path
|
|
22
|
+
# Check user custom preset first
|
|
23
|
+
user_path = File.join(@deploy_dir, "presets", @preset_name)
|
|
24
|
+
return user_path if File.exist?(user_path)
|
|
25
|
+
|
|
26
|
+
# Fall back to built-in
|
|
27
|
+
builtin_path = File.join(Kdep.templates_dir, "presets", @preset_name)
|
|
28
|
+
return builtin_path if File.exist?(builtin_path)
|
|
29
|
+
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Kdep
|
|
7
|
+
class Registry
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
attr_reader :host, :path_prefix
|
|
11
|
+
|
|
12
|
+
def initialize(registry_url)
|
|
13
|
+
url = registry_url.chomp("/")
|
|
14
|
+
parts = url.split("/", 2)
|
|
15
|
+
@host = parts[0]
|
|
16
|
+
@path_prefix = parts[1]
|
|
17
|
+
@registry_url = url
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def list_tags(image)
|
|
21
|
+
repository = "#{@path_prefix}/#{image}"
|
|
22
|
+
uri = URI("https://#{@host}/v2/#{repository}/tags/list")
|
|
23
|
+
|
|
24
|
+
req = Net::HTTP::Get.new(uri)
|
|
25
|
+
apply_auth(req, repository)
|
|
26
|
+
|
|
27
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
28
|
+
http.request(req)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return [] if response.code == "404"
|
|
32
|
+
|
|
33
|
+
unless response.code.start_with?("2")
|
|
34
|
+
raise Error, "Registry returned #{response.code}: #{response.body}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
data = JSON.parse(response.body)
|
|
38
|
+
data["tags"] || []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def apply_auth(request, repository)
|
|
44
|
+
if @host == "ghcr.io" || @host.end_with?(".ghcr.io")
|
|
45
|
+
apply_ghcr_auth(request, repository)
|
|
46
|
+
else
|
|
47
|
+
apply_docker_config_auth(request)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def apply_ghcr_auth(request, repository)
|
|
52
|
+
token = ENV["GHCR_TOKEN"]
|
|
53
|
+
if token && !token.empty?
|
|
54
|
+
request["Authorization"] = "Bearer #{token}"
|
|
55
|
+
return
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
token_uri = URI("https://ghcr.io/token?scope=repository:#{repository}:pull")
|
|
60
|
+
token_response = Net::HTTP.start(token_uri.host, token_uri.port, use_ssl: true) do |http|
|
|
61
|
+
http.request(Net::HTTP::Get.new(token_uri))
|
|
62
|
+
end
|
|
63
|
+
if token_response.code.start_with?("2")
|
|
64
|
+
data = JSON.parse(token_response.body)
|
|
65
|
+
if data["token"]
|
|
66
|
+
request["Authorization"] = "Bearer #{data["token"]}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Gracefully skip if token endpoint fails
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apply_docker_config_auth(request)
|
|
75
|
+
path = docker_config_path
|
|
76
|
+
return unless File.exist?(path)
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
config = JSON.parse(File.read(path))
|
|
80
|
+
auths = config["auths"] || {}
|
|
81
|
+
entry = auths[@host]
|
|
82
|
+
if entry && entry["auth"]
|
|
83
|
+
request["Authorization"] = "Basic #{entry["auth"]}"
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError
|
|
86
|
+
# Gracefully skip if config parsing fails
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def docker_config_path
|
|
91
|
+
File.join(Dir.home, ".docker", "config.json")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "yaml"
|
|
3
|
+
|
|
4
|
+
module Kdep
|
|
5
|
+
class Renderer
|
|
6
|
+
def initialize(config, deploy_dir)
|
|
7
|
+
@config = config
|
|
8
|
+
@deploy_dir = deploy_dir
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render_resource(resource_name)
|
|
12
|
+
template_path = resolve_template(resource_name)
|
|
13
|
+
raise "Template not found: #{resource_name}.yml.erb" unless template_path
|
|
14
|
+
|
|
15
|
+
template_content = File.read(template_path)
|
|
16
|
+
erb = if RUBY_VERSION >= "2.6"
|
|
17
|
+
ERB.new(template_content, trim_mode: "-")
|
|
18
|
+
else
|
|
19
|
+
ERB.new(template_content, nil, "-")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
secrets = {}
|
|
23
|
+
if resource_name == "secret"
|
|
24
|
+
secrets = load_secrets
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context = TemplateContext.new(@config, secrets)
|
|
28
|
+
erb.result(context.get_binding)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def resolve_template(resource_name)
|
|
34
|
+
# Check user override first
|
|
35
|
+
user_path = File.join(@deploy_dir, "resources", "#{resource_name}.yml.erb")
|
|
36
|
+
return user_path if File.exist?(user_path)
|
|
37
|
+
|
|
38
|
+
# Fall back to built-in
|
|
39
|
+
builtin_path = File.join(Kdep.templates_dir, "resources", "#{resource_name}.yml.erb")
|
|
40
|
+
return builtin_path if File.exist?(builtin_path)
|
|
41
|
+
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def load_secrets
|
|
46
|
+
secrets_path = File.join(@deploy_dir, "secrets.yml")
|
|
47
|
+
return {} unless File.exist?(secrets_path)
|
|
48
|
+
|
|
49
|
+
content = File.read(secrets_path)
|
|
50
|
+
YAML.safe_load(content) || {}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Kdep
|
|
2
|
+
class TemplateContext
|
|
3
|
+
def initialize(config, secrets = {})
|
|
4
|
+
@config = config
|
|
5
|
+
@secrets = secrets
|
|
6
|
+
|
|
7
|
+
# Expose top-level config keys as methods for cleaner templates
|
|
8
|
+
config.each do |key, value|
|
|
9
|
+
define_singleton_method(key.to_sym) { value } if key.is_a?(String)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get_binding
|
|
14
|
+
binding
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Helper: render a value as YAML-safe string
|
|
18
|
+
def yaml_value(val)
|
|
19
|
+
val.is_a?(String) ? val : val.to_s
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|