kdep 0.1.3 → 0.2.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/cli.rb +1 -0
- data/lib/kdep/commands/apply.rb +1 -1
- data/lib/kdep/commands/bump.rb +17 -10
- data/lib/kdep/commands/diff.rb +1 -1
- data/lib/kdep/commands/helm_install.rb +286 -0
- data/lib/kdep/commands/migrate.rb +176 -71
- data/lib/kdep/commands/render.rb +2 -2
- data/lib/kdep/defaults.rb +6 -2
- data/lib/kdep/docker.rb +57 -1
- data/lib/kdep/helm.rb +34 -0
- data/lib/kdep/old_format.rb +690 -9
- data/lib/kdep/preset.rb +1 -1
- data/lib/kdep/registry.rb +119 -3
- data/lib/kdep/version.rb +1 -1
- data/lib/kdep.rb +2 -0
- data/templates/presets/helm +4 -0
- data/templates/presets/statefulset +4 -0
- data/templates/presets/statefulset_svc +5 -0
- data/templates/resources/configmap.yml.erb +7 -3
- data/templates/resources/cronjob.yml.erb +49 -12
- data/templates/resources/deployment.yml.erb +64 -11
- data/templates/resources/ingress.yml.erb +59 -13
- data/templates/resources/secret.yml.erb +7 -7
- data/templates/resources/service.yml.erb +26 -2
- data/templates/resources/statefulset.yml.erb +125 -0
- metadata +16 -10
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "optparse"
|
|
2
2
|
require "fileutils"
|
|
3
3
|
require "yaml"
|
|
4
|
+
require "tmpdir"
|
|
4
5
|
|
|
5
6
|
module Kdep
|
|
6
7
|
module Commands
|
|
@@ -9,8 +10,9 @@ module Kdep
|
|
|
9
10
|
OptionParser.new do |opts|
|
|
10
11
|
opts.banner = "Usage: kdep migrate [path]"
|
|
11
12
|
opts.separator ""
|
|
12
|
-
opts.separator "Converts old deploy
|
|
13
|
-
opts.separator "
|
|
13
|
+
opts.separator "Converts old deploy directories to kdep format."
|
|
14
|
+
opts.separator "Supports both old pattern (.app-name + deploy/) and"
|
|
15
|
+
opts.separator "new pattern (deploy_web/app.yml + subdirectories)."
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
|
|
@@ -37,8 +39,8 @@ module Kdep
|
|
|
37
39
|
# Parse old format
|
|
38
40
|
old = Kdep::OldFormat.new(path)
|
|
39
41
|
begin
|
|
40
|
-
name = old.app_name
|
|
41
42
|
kdep_config = old.to_kdep_config
|
|
43
|
+
name = kdep_config["name"] || old.app_name
|
|
42
44
|
rescue => e
|
|
43
45
|
@ui.error(e.message)
|
|
44
46
|
exit 1
|
|
@@ -55,102 +57,205 @@ module Kdep
|
|
|
55
57
|
File.write(File.join(target_dir, "app.yml"), YAML.dump(kdep_config))
|
|
56
58
|
@ui.file_written("kdep/#{name}/app.yml")
|
|
57
59
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
+
# Extract and write env vars from ConfigMap
|
|
61
|
+
env = old.extract_env
|
|
62
|
+
unless env.empty?
|
|
63
|
+
@ui.info("Extracted #{env.size} env vars from ConfigMap (already in envFrom refs)")
|
|
64
|
+
end
|
|
60
65
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
# Extract and write secrets (decode base64 =>plain text for secrets.yml)
|
|
67
|
+
secrets = old.extract_secrets
|
|
68
|
+
if secrets.empty?
|
|
69
|
+
@ui.warn("No secrets found. Create kdep/#{name}/secrets.yml manually if needed.")
|
|
70
|
+
else
|
|
71
|
+
require "base64"
|
|
72
|
+
decoded = {}
|
|
73
|
+
secrets.each do |k, v|
|
|
74
|
+
decoded[k] = Base64.decode64(v) rescue v
|
|
75
|
+
end
|
|
76
|
+
secrets_path = File.join(target_dir, "secrets.yml")
|
|
77
|
+
File.write(secrets_path, YAML.dump(decoded))
|
|
78
|
+
@ui.file_written("kdep/#{name}/secrets.yml")
|
|
79
|
+
@ui.warn("#{secrets.size} secrets written. Review kdep/#{name}/secrets.yml — values are now plain text.")
|
|
80
|
+
end
|
|
65
81
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
# Generate custom resource templates (PVC, stringData Secrets)
|
|
83
|
+
custom_resources = old.extract_custom_resources
|
|
84
|
+
unless custom_resources.empty?
|
|
85
|
+
res_dir = File.join(target_dir, "resources")
|
|
86
|
+
FileUtils.mkdir_p(res_dir)
|
|
87
|
+
custom_resources.each do |cr|
|
|
88
|
+
kind = cr["kind"]
|
|
89
|
+
doc = cr["doc"]
|
|
90
|
+
res_name = doc.dig("metadata", "name") || kind.downcase
|
|
91
|
+
filename = "#{res_name.gsub(/[^a-zA-Z0-9_-]/, "-")}.yml.erb"
|
|
92
|
+
File.write(File.join(res_dir, filename), YAML.dump(doc))
|
|
93
|
+
@ui.file_written("kdep/#{name}/resources/#{filename}")
|
|
94
|
+
end
|
|
95
|
+
@ui.info("#{custom_resources.size} custom resource(s) generated as templates")
|
|
96
|
+
end
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
@ui.success("Migration complete: kdep/#{name}/")
|
|
99
|
+
@ui.info("")
|
|
100
|
+
@ui.info("Generated config:")
|
|
101
|
+
kdep_config.each do |k, v|
|
|
102
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
|
103
|
+
@ui.info(" #{k}: #{v.inspect}")
|
|
104
|
+
else
|
|
105
|
+
@ui.info(" #{k}: #{v}")
|
|
106
|
+
end
|
|
76
107
|
end
|
|
77
108
|
|
|
78
|
-
#
|
|
109
|
+
# Automatic post-migration verification
|
|
110
|
+
@ui.info("")
|
|
111
|
+
verify_migration(old, target_dir, kdep_config)
|
|
112
|
+
|
|
113
|
+
@ui.info("")
|
|
114
|
+
@ui.info("Next: review kdep/#{name}/app.yml, then `kdep render #{name}` to test")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def verify_migration(old, target_dir, kdep_config)
|
|
120
|
+
@ui.info("Verifying migration...")
|
|
121
|
+
|
|
79
122
|
old_files = old.materialized_files
|
|
80
123
|
if old_files.empty?
|
|
81
|
-
@ui.warn("No
|
|
82
|
-
@ui.success("Migration complete: kdep/#{name}/")
|
|
124
|
+
@ui.warn("No old manifests found to verify against")
|
|
83
125
|
return
|
|
84
126
|
end
|
|
85
127
|
|
|
86
|
-
|
|
128
|
+
old_by_kind = parse_yamls(old_files)
|
|
87
129
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
# Render new manifests to a temp dir
|
|
131
|
+
config = Kdep::Config.new(target_dir).load
|
|
132
|
+
preset = Kdep::Preset.new(config["preset"], target_dir)
|
|
133
|
+
|
|
134
|
+
Dir.mktmpdir("kdep-verify") do |tmpdir|
|
|
135
|
+
output_dir = File.join(tmpdir, ".rendered")
|
|
136
|
+
writer = Kdep::Writer.new(output_dir)
|
|
137
|
+
writer.clean
|
|
138
|
+
renderer = Kdep::Renderer.new(config, target_dir)
|
|
139
|
+
|
|
140
|
+
rendered_files = []
|
|
141
|
+
preset.resources.each_with_index do |res, idx|
|
|
142
|
+
content = renderer.render_resource(res)
|
|
143
|
+
path = writer.write(res, content, idx + 1)
|
|
144
|
+
rendered_files << path if path
|
|
96
145
|
end
|
|
97
|
-
# Clean up failed migration
|
|
98
|
-
FileUtils.rm_rf(target_dir)
|
|
99
|
-
@ui.error("Migration aborted. Generated config removed.")
|
|
100
|
-
exit 1
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
146
|
|
|
104
|
-
|
|
147
|
+
new_by_kind = parse_yamls(rendered_files)
|
|
105
148
|
|
|
106
|
-
|
|
107
|
-
mismatches = []
|
|
108
|
-
# Match by filename (basename)
|
|
109
|
-
old_by_name = {}
|
|
110
|
-
old_files.each { |f| old_by_name[File.basename(f)] = f }
|
|
149
|
+
diffs = collect_diffs(old_by_kind, new_by_kind)
|
|
111
150
|
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
if diffs.empty?
|
|
152
|
+
@ui.success("Verification passed: all fields match")
|
|
153
|
+
else
|
|
154
|
+
field_count = diffs.count { |d| d.include?(" ~ ") || d.include?(" + ") || d.include?(" - ") }
|
|
155
|
+
@ui.info("")
|
|
156
|
+
@ui.info(colorize(:bold, "Verification diff (#{field_count} field diffs, old => new):"))
|
|
157
|
+
diffs.each { |d| puts d }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue => e
|
|
161
|
+
@ui.warn("Verification failed: #{e.message}")
|
|
162
|
+
end
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
164
|
+
def collect_diffs(old_by_kind, new_by_kind)
|
|
165
|
+
diffs = []
|
|
166
|
+
old_by_kind.each do |kind, old_entry|
|
|
167
|
+
new_entry = new_by_kind[kind]
|
|
168
|
+
unless new_entry
|
|
169
|
+
diffs << colorize(:red, " #{kind}: MISSING in rendered")
|
|
120
170
|
next
|
|
121
171
|
end
|
|
172
|
+
kind_diffs = tree_diff(old_entry["doc"], new_entry["doc"])
|
|
173
|
+
unless kind_diffs.empty?
|
|
174
|
+
diffs << colorize(:bold, " #{kind}:")
|
|
175
|
+
diffs.concat(kind_diffs)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
new_by_kind.each do |kind, _|
|
|
179
|
+
unless old_by_kind.keys.any? { |k| k.start_with?(kind) }
|
|
180
|
+
diffs << colorize(:yellow, " #{kind}: EXTRA in rendered")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
diffs
|
|
184
|
+
end
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
186
|
+
def tree_diff(old_val, new_val, path = "")
|
|
187
|
+
old_val = normalize(old_val)
|
|
188
|
+
new_val = normalize(new_val)
|
|
189
|
+
diffs = []
|
|
125
190
|
|
|
126
|
-
|
|
127
|
-
|
|
191
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
192
|
+
(old_val.keys + new_val.keys).uniq.sort.each do |k|
|
|
193
|
+
p = path.empty? ? k.to_s : "#{path}.#{k}"
|
|
194
|
+
if !old_val.key?(k)
|
|
195
|
+
diffs << colorize(:green, " + #{p}: #{fmt(new_val[k])}")
|
|
196
|
+
elsif !new_val.key?(k)
|
|
197
|
+
diffs << colorize(:red, " - #{p}: #{fmt(old_val[k])}")
|
|
198
|
+
else
|
|
199
|
+
diffs.concat(tree_diff(old_val[k], new_val[k], p))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
elsif old_val.is_a?(Array) && new_val.is_a?(Array)
|
|
203
|
+
[old_val.size, new_val.size].max.times do |i|
|
|
204
|
+
p = "#{path}[#{i}]"
|
|
205
|
+
if i >= old_val.size
|
|
206
|
+
diffs << colorize(:green, " + #{p}: #{fmt(new_val[i])}")
|
|
207
|
+
elsif i >= new_val.size
|
|
208
|
+
diffs << colorize(:red, " - #{p}: #{fmt(old_val[i])}")
|
|
209
|
+
else
|
|
210
|
+
diffs.concat(tree_diff(old_val[i], new_val[i], p))
|
|
211
|
+
end
|
|
128
212
|
end
|
|
213
|
+
elsif old_val != new_val
|
|
214
|
+
diffs << colorize(:yellow, " ~ #{p || path}: #{fmt(old_val)} => #{fmt(new_val)}")
|
|
129
215
|
end
|
|
216
|
+
diffs
|
|
217
|
+
end
|
|
130
218
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
219
|
+
def normalize(obj)
|
|
220
|
+
case obj
|
|
221
|
+
when Hash
|
|
222
|
+
obj.sort_by { |k, _| k.to_s }.map { |k, v| [k, normalize(v)] }.to_h
|
|
223
|
+
when Array
|
|
224
|
+
obj.map { |v| normalize(v) }
|
|
225
|
+
when String
|
|
226
|
+
# Normalize image tags — tags are managed by kdep bump
|
|
227
|
+
obj.match?(/:\d+[\.\d]*\z/) ? obj.sub(/:[^:]+\z/, ":TAG") : obj
|
|
228
|
+
else
|
|
229
|
+
obj
|
|
136
230
|
end
|
|
231
|
+
end
|
|
137
232
|
|
|
138
|
-
|
|
233
|
+
def fmt(val)
|
|
234
|
+
case val
|
|
235
|
+
when Hash, Array then val.inspect[0, 80]
|
|
236
|
+
else val.inspect
|
|
237
|
+
end
|
|
139
238
|
end
|
|
140
239
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
240
|
+
def parse_yamls(files)
|
|
241
|
+
by_kind = {}
|
|
242
|
+
files.each do |f|
|
|
243
|
+
File.read(f).split(/^---\s*$/).each do |part|
|
|
244
|
+
next if part.strip.empty?
|
|
245
|
+
doc = YAML.safe_load(part) rescue next
|
|
246
|
+
next unless doc.is_a?(Hash) && doc["kind"]
|
|
247
|
+
kind = doc["kind"]
|
|
248
|
+
kind = "#{kind}_#{by_kind.keys.count { |k| k.start_with?(doc["kind"]) }}" if by_kind[kind]
|
|
249
|
+
by_kind[kind] = { "doc" => doc, "file" => f }
|
|
250
|
+
end
|
|
152
251
|
end
|
|
153
|
-
|
|
252
|
+
by_kind
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def colorize(color, text)
|
|
256
|
+
return text unless $stdout.tty?
|
|
257
|
+
colors = { red: "\e[31m", green: "\e[32m", yellow: "\e[33m", bold: "\e[1m" }
|
|
258
|
+
"#{colors[color]}#{text}\e[0m"
|
|
154
259
|
end
|
|
155
260
|
end
|
|
156
261
|
end
|
data/lib/kdep/commands/render.rb
CHANGED
|
@@ -44,9 +44,9 @@ module Kdep
|
|
|
44
44
|
preset = Kdep::Preset.new(config["preset"], deploy_dir)
|
|
45
45
|
resources = preset.resources
|
|
46
46
|
|
|
47
|
-
# Determine output directory (.rendered/ in
|
|
47
|
+
# Determine output directory (.rendered/ in deploy target dir)
|
|
48
48
|
repo_root = File.dirname(kdep_dir)
|
|
49
|
-
output_dir = File.join(
|
|
49
|
+
output_dir = File.join(deploy_dir, ".rendered")
|
|
50
50
|
|
|
51
51
|
# Set up writer and renderer
|
|
52
52
|
writer = Kdep::Writer.new(output_dir)
|
data/lib/kdep/defaults.rb
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
module Kdep
|
|
2
2
|
module Defaults
|
|
3
3
|
BASE_DEFAULTS = {
|
|
4
|
-
"port" => 8080,
|
|
5
4
|
"replicas" => 1,
|
|
5
|
+
"namespace" => "default",
|
|
6
|
+
"platform" => "linux/amd64",
|
|
6
7
|
}.freeze
|
|
7
8
|
|
|
8
9
|
PRESET_OVERRIDES = {
|
|
9
|
-
"web" => { "replicas" => 3 },
|
|
10
|
+
"web" => { "replicas" => 3, "port" => 8080 },
|
|
10
11
|
"worker" => {},
|
|
11
12
|
"job" => {},
|
|
12
13
|
"cronjob" => {},
|
|
14
|
+
"statefulset" => {},
|
|
15
|
+
"statefulset_svc" => {},
|
|
16
|
+
"helm" => {},
|
|
13
17
|
"custom" => {},
|
|
14
18
|
}.freeze
|
|
15
19
|
|
data/lib/kdep/docker.rb
CHANGED
|
@@ -4,11 +4,16 @@ module Kdep
|
|
|
4
4
|
module Docker
|
|
5
5
|
class Error < StandardError; end
|
|
6
6
|
|
|
7
|
-
def self.build(tag:, context_dir:, dockerfile: nil, target: nil, platform: nil)
|
|
7
|
+
def self.build(tag:, context_dir:, dockerfile: nil, target: nil, platform: nil, build_args: nil)
|
|
8
|
+
ensure_login(tag)
|
|
9
|
+
|
|
8
10
|
args = ["docker", "build", "-t", tag]
|
|
9
11
|
args.push("--file", dockerfile) if dockerfile
|
|
10
12
|
args.push("--target", target) if target
|
|
11
13
|
args.push("--platform", platform) if platform
|
|
14
|
+
if build_args.is_a?(Hash)
|
|
15
|
+
build_args.each { |k, v| args.push("--build-arg", "#{k}=#{v}") }
|
|
16
|
+
end
|
|
12
17
|
args.push(context_dir)
|
|
13
18
|
|
|
14
19
|
stdout, stderr, status = Open3.capture3(*args)
|
|
@@ -19,11 +24,62 @@ module Kdep
|
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
def self.push(tag)
|
|
27
|
+
ensure_login(tag)
|
|
28
|
+
|
|
22
29
|
stdout, stderr, status = Open3.capture3("docker", "push", tag)
|
|
23
30
|
unless status.success?
|
|
24
31
|
raise Error, "docker push failed: #{stderr.strip}"
|
|
25
32
|
end
|
|
26
33
|
stdout
|
|
27
34
|
end
|
|
35
|
+
|
|
36
|
+
def self.ensure_login(tag)
|
|
37
|
+
server = tag.split("/").first
|
|
38
|
+
return unless server && server.include?(".")
|
|
39
|
+
|
|
40
|
+
@logged_in ||= {}
|
|
41
|
+
return if @logged_in[server]
|
|
42
|
+
|
|
43
|
+
user, pass = read_credentials(server)
|
|
44
|
+
return unless user && pass
|
|
45
|
+
|
|
46
|
+
_, stderr, status = Open3.capture3(
|
|
47
|
+
"docker", "login", server, "-u", user, "--password-stdin",
|
|
48
|
+
stdin_data: pass
|
|
49
|
+
)
|
|
50
|
+
if status.success?
|
|
51
|
+
@logged_in[server] = true
|
|
52
|
+
else
|
|
53
|
+
$stderr.puts "warning: docker login to #{server} failed: #{stderr.strip}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.read_credentials(server)
|
|
58
|
+
# Try ~/.leadfycr (default for leadfycr.azurecr.io)
|
|
59
|
+
cred_file = File.join(Dir.home, ".leadfycr")
|
|
60
|
+
if File.exist?(cred_file)
|
|
61
|
+
parts = File.read(cred_file).strip.split(":", 2)
|
|
62
|
+
return parts if parts.length == 2
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Try ~/.docker/config.json
|
|
66
|
+
config_path = File.join(Dir.home, ".docker", "config.json")
|
|
67
|
+
if File.exist?(config_path)
|
|
68
|
+
begin
|
|
69
|
+
config = JSON.parse(File.read(config_path))
|
|
70
|
+
entry = (config["auths"] || {})[server]
|
|
71
|
+
if entry && entry["auth"]
|
|
72
|
+
decoded = Base64.decode64(entry["auth"])
|
|
73
|
+
parts = decoded.split(":", 2)
|
|
74
|
+
return parts if parts.length == 2
|
|
75
|
+
end
|
|
76
|
+
rescue StandardError
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
[nil, nil]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method :ensure_login, :read_credentials
|
|
28
84
|
end
|
|
29
85
|
end
|
data/lib/kdep/helm.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
module Helm
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
def self.run(*args)
|
|
8
|
+
stdout, stderr, status = Open3.capture3("helm", *args)
|
|
9
|
+
unless status.success?
|
|
10
|
+
raise Error, "helm #{args.first} failed: #{stderr.strip}"
|
|
11
|
+
end
|
|
12
|
+
stdout
|
|
13
|
+
end
|
|
14
|
+
|
|
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)
|
|
17
|
+
args = ["upgrade", "--install", release, chart, "-n", namespace]
|
|
18
|
+
args += ["-f", values_file] if values_file
|
|
19
|
+
sets.each do |key, value|
|
|
20
|
+
args += ["--set", "#{key}=#{value}"]
|
|
21
|
+
end
|
|
22
|
+
args << "--dry-run" if dry_run
|
|
23
|
+
run(*args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.repo_add(name, url)
|
|
27
|
+
run("repo", "add", name, url, "--force-update")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.repo_update
|
|
31
|
+
run("repo", "update")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|