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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/exe/kdep +3 -0
  4. data/lib/kdep/cli.rb +72 -0
  5. data/lib/kdep/cluster_health.rb +44 -0
  6. data/lib/kdep/commands/apply.rb +147 -0
  7. data/lib/kdep/commands/build.rb +103 -0
  8. data/lib/kdep/commands/bump.rb +190 -0
  9. data/lib/kdep/commands/diff.rb +133 -0
  10. data/lib/kdep/commands/eject.rb +97 -0
  11. data/lib/kdep/commands/init.rb +99 -0
  12. data/lib/kdep/commands/log.rb +145 -0
  13. data/lib/kdep/commands/push.rb +94 -0
  14. data/lib/kdep/commands/render.rb +130 -0
  15. data/lib/kdep/commands/restart.rb +109 -0
  16. data/lib/kdep/commands/scale.rb +136 -0
  17. data/lib/kdep/commands/secrets.rb +223 -0
  18. data/lib/kdep/commands/sh.rb +117 -0
  19. data/lib/kdep/commands/status.rb +187 -0
  20. data/lib/kdep/config.rb +69 -0
  21. data/lib/kdep/context_guard.rb +23 -0
  22. data/lib/kdep/dashboard/health_panel.rb +53 -0
  23. data/lib/kdep/dashboard/layout.rb +40 -0
  24. data/lib/kdep/dashboard/log_panel.rb +54 -0
  25. data/lib/kdep/dashboard/panel.rb +135 -0
  26. data/lib/kdep/dashboard/resources_panel.rb +91 -0
  27. data/lib/kdep/dashboard/rollout_panel.rb +75 -0
  28. data/lib/kdep/dashboard/screen.rb +29 -0
  29. data/lib/kdep/dashboard.rb +258 -0
  30. data/lib/kdep/defaults.rb +21 -0
  31. data/lib/kdep/discovery.rb +29 -0
  32. data/lib/kdep/docker.rb +29 -0
  33. data/lib/kdep/kubectl.rb +41 -0
  34. data/lib/kdep/preset.rb +33 -0
  35. data/lib/kdep/registry.rb +94 -0
  36. data/lib/kdep/renderer.rb +53 -0
  37. data/lib/kdep/template_context.rb +22 -0
  38. data/lib/kdep/ui.rb +48 -0
  39. data/lib/kdep/validator.rb +73 -0
  40. data/lib/kdep/version.rb +3 -0
  41. data/lib/kdep/version_tagger.rb +27 -0
  42. data/lib/kdep/writer.rb +26 -0
  43. data/lib/kdep.rb +49 -0
  44. data/templates/init/app.yml.erb +49 -0
  45. data/templates/init/gitignore +3 -0
  46. data/templates/init/secrets.yml +7 -0
  47. data/templates/presets/cronjob +4 -0
  48. data/templates/presets/job +4 -0
  49. data/templates/presets/web +7 -0
  50. data/templates/presets/worker +4 -0
  51. data/templates/resources/configmap.yml.erb +7 -0
  52. data/templates/resources/cronjob.yml.erb +42 -0
  53. data/templates/resources/deployment.yml.erb +71 -0
  54. data/templates/resources/ingress.yml.erb +47 -0
  55. data/templates/resources/job.yml.erb +35 -0
  56. data/templates/resources/secret.yml.erb +15 -0
  57. data/templates/resources/service.yml.erb +13 -0
  58. metadata +142 -0
@@ -0,0 +1,133 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Diff
6
+ def self.option_parser
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: kdep diff [deploy] [env]"
9
+ opts.separator ""
10
+ opts.separator "Shows differences between rendered manifests and live cluster state."
11
+ end
12
+ end
13
+
14
+ def initialize(global_options:, command_options:, args:)
15
+ @global_options = global_options
16
+ @command_options = command_options
17
+ @args = args
18
+ @ui = Kdep::UI.new(color: false)
19
+ end
20
+
21
+ def execute
22
+ deploy_name = @args[0]
23
+ env = @args[1]
24
+
25
+ # Discover kdep/ directory
26
+ discovery = Kdep::Discovery.new
27
+ kdep_dir = discovery.find_kdep_dir
28
+
29
+ unless kdep_dir
30
+ @ui.error("No kdep/ directory found")
31
+ exit 1
32
+ end
33
+
34
+ # Resolve deploy directory
35
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
36
+ unless deploy_dir
37
+ exit 1
38
+ end
39
+
40
+ # Load config
41
+ config = Kdep::Config.new(deploy_dir, env).load
42
+
43
+ # Validate context before any cluster operation
44
+ begin
45
+ Kdep::ContextGuard.new(config["context"]).validate!
46
+ rescue Kdep::Kubectl::Error => e
47
+ @ui.error(e.message)
48
+ exit 1
49
+ end
50
+
51
+ # Load preset resources and render
52
+ preset = Kdep::Preset.new(config["preset"], deploy_dir)
53
+ resources = preset.resources
54
+
55
+ repo_root = File.dirname(kdep_dir)
56
+ output_dir = File.join(repo_root, ".rendered")
57
+
58
+ writer = Kdep::Writer.new(output_dir)
59
+ writer.clean
60
+ renderer = Kdep::Renderer.new(config, deploy_dir)
61
+
62
+ resources.each_with_index do |resource_name, idx|
63
+ index = idx + 1
64
+ begin
65
+ content = renderer.render_resource(resource_name)
66
+ rescue => e
67
+ @ui.error("#{resource_name}: #{e.message}")
68
+ next
69
+ end
70
+ writer.write(resource_name, content, index)
71
+ end
72
+
73
+ # Get sorted rendered files
74
+ rendered_files = Dir.glob(File.join(output_dir, "*.yml")).sort
75
+
76
+ if rendered_files.empty?
77
+ @ui.info("No rendered files to diff")
78
+ return
79
+ end
80
+
81
+ # Diff each file against live cluster state
82
+ has_diffs = false
83
+ errors = []
84
+
85
+ rendered_files.each do |file_path|
86
+ begin
87
+ diff_output = Kdep::Kubectl.diff(file_path)
88
+ if diff_output
89
+ has_diffs = true
90
+ @ui.info("--- #{File.basename(file_path)} ---")
91
+ @ui.info(diff_output)
92
+ end
93
+ rescue Kdep::Kubectl::Error => e
94
+ errors << { "file" => File.basename(file_path), "error" => e.message }
95
+ @ui.error("#{File.basename(file_path)}: #{e.message}")
96
+ end
97
+ end
98
+
99
+ unless has_diffs || errors.any?
100
+ @ui.info("No changes -- cluster state matches rendered manifests.")
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
107
+ if deploy_name
108
+ deploy_dir = File.join(kdep_dir, deploy_name)
109
+ unless File.directory?(deploy_dir)
110
+ @ui.error("Deploy target not found: #{deploy_name}")
111
+ deploys = discovery.find_deploys
112
+ if deploys.any?
113
+ @ui.info("Available deploys: #{deploys.join(', ')}")
114
+ end
115
+ return nil
116
+ end
117
+ deploy_dir
118
+ else
119
+ deploys = discovery.find_deploys
120
+ if deploys.length == 1
121
+ File.join(kdep_dir, deploys[0])
122
+ elsif deploys.length > 1
123
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
124
+ nil
125
+ else
126
+ @ui.error("No deploy targets found in kdep/")
127
+ nil
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,97 @@
1
+ require "optparse"
2
+ require "fileutils"
3
+
4
+ module Kdep
5
+ module Commands
6
+ class Eject
7
+ def self.option_parser
8
+ OptionParser.new do |opts|
9
+ opts.banner = "Usage: kdep eject [resource_name]"
10
+ opts.separator ""
11
+ opts.separator "Copies a built-in template to your deploy target for customization."
12
+ end
13
+ end
14
+
15
+ def initialize(global_options:, command_options:, args:)
16
+ @global_options = global_options
17
+ @command_options = command_options
18
+ @args = args
19
+ @ui = Kdep::UI.new
20
+ end
21
+
22
+ def execute
23
+ resource_name = @args[0]
24
+
25
+ if resource_name.nil?
26
+ list_available_resources
27
+ return
28
+ end
29
+
30
+ # Find kdep dir
31
+ discovery = Kdep::Discovery.new
32
+ kdep_dir = discovery.find_kdep_dir
33
+
34
+ unless kdep_dir
35
+ @ui.error("No kdep/ directory found (run kdep init first)")
36
+ exit 1
37
+ end
38
+
39
+ # Find deploy directory
40
+ deploys = discovery.find_deploys
41
+ if deploys.empty?
42
+ @ui.error("No deploy targets found in #{kdep_dir}/")
43
+ exit 1
44
+ end
45
+
46
+ deploy_name = deploys.first
47
+ deploy_dir = File.join(kdep_dir, deploy_name)
48
+
49
+ # Check built-in template exists
50
+ source = File.join(Kdep.templates_dir, "resources", "#{resource_name}.yml.erb")
51
+ unless File.exist?(source)
52
+ @ui.error("Unknown resource: #{resource_name}")
53
+ @ui.info("Available resources: #{available_resources.join(', ')}")
54
+ exit 1
55
+ end
56
+
57
+ # Target path
58
+ resources_dir = File.join(deploy_dir, "resources")
59
+ target = File.join(resources_dir, "#{resource_name}.yml.erb")
60
+
61
+ if File.exist?(target)
62
+ @ui.warn("#{resource_name}.yml.erb already exists in resources/ -- skipping (delete it first to re-eject)")
63
+ return
64
+ end
65
+
66
+ # Create resources dir and copy
67
+ FileUtils.mkdir_p(resources_dir)
68
+ FileUtils.cp(source, target)
69
+
70
+ @ui.file_written(target)
71
+ @ui.info("Edit #{target} to customize the #{resource_name} template")
72
+ end
73
+
74
+ private
75
+
76
+ def list_available_resources
77
+ resources = available_resources
78
+ if resources.empty?
79
+ @ui.info("No built-in resource templates found")
80
+ else
81
+ @ui.info("Available resources to eject:")
82
+ resources.each { |r| @ui.info(" #{r}") }
83
+ end
84
+ end
85
+
86
+ def available_resources
87
+ resources_dir = File.join(Kdep.templates_dir, "resources")
88
+ return [] unless File.directory?(resources_dir)
89
+
90
+ Dir.entries(resources_dir)
91
+ .select { |f| f.end_with?(".yml.erb") }
92
+ .map { |f| f.sub(".yml.erb", "") }
93
+ .sort
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,99 @@
1
+ require "optparse"
2
+ require "erb"
3
+ require "fileutils"
4
+
5
+ module Kdep
6
+ module Commands
7
+ class Init
8
+ def self.option_parser
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: kdep init [preset] [name]"
11
+ opts.separator ""
12
+ opts.separator "Scaffolds a new kdep deploy target."
13
+ opts.on("--namespace=NAMESPACE", "Kubernetes namespace")
14
+ opts.on("--registry=REGISTRY", "Container registry URL")
15
+ end
16
+ end
17
+
18
+ def initialize(global_options:, command_options:, args:)
19
+ @global_options = global_options
20
+ @command_options = command_options
21
+ @args = args
22
+ @ui = Kdep::UI.new
23
+ end
24
+
25
+ def execute
26
+ preset = @args[0]
27
+ deploy_name = @args[1]
28
+
29
+ unless preset
30
+ @ui.error("Missing preset and name arguments")
31
+ @ui.info("Usage: kdep init [preset] [name]")
32
+ @ui.info("Available presets: #{Kdep::Preset::BUILT_IN.join(', ')}")
33
+ exit 1
34
+ end
35
+
36
+ unless deploy_name
37
+ @ui.error("Missing name argument")
38
+ @ui.info("Usage: kdep init [preset] [name]")
39
+ exit 1
40
+ end
41
+
42
+ unless Kdep::Preset::BUILT_IN.include?(preset)
43
+ @ui.error("Unknown preset: #{preset}")
44
+ @ui.info("Available presets: #{Kdep::Preset::BUILT_IN.join(', ')}")
45
+ exit 1
46
+ end
47
+
48
+ target_dir = File.join(Dir.pwd, "kdep", deploy_name)
49
+
50
+ if File.exist?(target_dir)
51
+ @ui.error("kdep/#{deploy_name} already exists")
52
+ exit 1
53
+ end
54
+
55
+ FileUtils.mkdir_p(target_dir)
56
+
57
+ # Render app.yml from ERB template
58
+ namespace = @command_options[:namespace]
59
+ registry = @command_options[:registry]
60
+
61
+ template_path = File.join(Kdep.templates_dir, "init", "app.yml.erb")
62
+ template = File.read(template_path)
63
+ erb = if RUBY_VERSION >= "2.6"
64
+ ERB.new(template, trim_mode: "-")
65
+ else
66
+ ERB.new(template, nil, "-")
67
+ end
68
+ binding_ctx = erb_binding(preset: preset, deploy_name: deploy_name,
69
+ namespace: namespace, registry: registry)
70
+ app_yml_content = erb.result(binding_ctx)
71
+
72
+ File.write(File.join(target_dir, "app.yml"), app_yml_content)
73
+ @ui.file_written("kdep/#{deploy_name}/app.yml")
74
+
75
+ # Copy secrets.yml
76
+ FileUtils.cp(
77
+ File.join(Kdep.templates_dir, "init", "secrets.yml"),
78
+ File.join(target_dir, "secrets.yml")
79
+ )
80
+ @ui.file_written("kdep/#{deploy_name}/secrets.yml")
81
+
82
+ # Copy .gitignore
83
+ FileUtils.cp(
84
+ File.join(Kdep.templates_dir, "init", "gitignore"),
85
+ File.join(target_dir, ".gitignore")
86
+ )
87
+ @ui.file_written("kdep/#{deploy_name}/.gitignore")
88
+
89
+ @ui.success("Initialized kdep/#{deploy_name} with #{preset} preset")
90
+ end
91
+
92
+ private
93
+
94
+ def erb_binding(preset:, deploy_name:, namespace:, registry:)
95
+ binding
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,145 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Log
6
+ def self.option_parser
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: kdep log [deploy]"
9
+ opts.separator ""
10
+ opts.separator "Streams pod logs with error highlighting."
11
+ opts.separator ""
12
+ opts.on("-f", "--[no-]follow", "Follow log output (default: true)")
13
+ opts.on("-c", "--container=NAME", "Container name for multi-container pods")
14
+ opts.on("--tail=N", Integer, "Number of lines to show (default: 100)")
15
+ end
16
+ end
17
+
18
+ def initialize(global_options:, command_options:, args:)
19
+ @global_options = global_options
20
+ @command_options = command_options
21
+ @args = args
22
+ @ui = Kdep::UI.new(color: false)
23
+ end
24
+
25
+ def execute
26
+ deploy_name = @args[0]
27
+
28
+ # Discover kdep/ directory
29
+ discovery = Kdep::Discovery.new
30
+ kdep_dir = discovery.find_kdep_dir
31
+ unless kdep_dir
32
+ @ui.error("No kdep/ directory found")
33
+ exit 1
34
+ end
35
+
36
+ # Resolve deploy directory
37
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
38
+ unless deploy_dir
39
+ exit 1
40
+ end
41
+
42
+ # Load config
43
+ config = Kdep::Config.new(deploy_dir, nil).load
44
+
45
+ # Validate context
46
+ Kdep::ContextGuard.new(config["context"]).validate!
47
+
48
+ # Find running pod
49
+ namespace = config["namespace"]
50
+ app_name = config["name"]
51
+ pod_name = find_pod(namespace, app_name)
52
+
53
+ # Stream logs
54
+ stream_logs(pod_name, namespace)
55
+ end
56
+
57
+ private
58
+
59
+ def find_pod(namespace, app_name)
60
+ data = Kdep::Kubectl.run_json(
61
+ "get", "pods",
62
+ "-n", namespace,
63
+ "-l", "app=#{app_name}",
64
+ "--field-selector", "status.phase=Running"
65
+ )
66
+ pods = data["items"]
67
+
68
+ if pods.nil? || pods.empty?
69
+ @ui.error("No running pods found for #{app_name} in #{namespace}")
70
+ exit 1
71
+ end
72
+
73
+ pods[0]["metadata"]["name"]
74
+ end
75
+
76
+ def stream_logs(pod_name, namespace)
77
+ follow = @command_options.fetch(:follow, true)
78
+ tail = @command_options.fetch(:tail, 100)
79
+ container = @command_options[:container]
80
+
81
+ args = ["kubectl", "logs", pod_name, "-n", namespace]
82
+ args << "-f" if follow
83
+ args << "--tail=#{tail}"
84
+
85
+ if container
86
+ args << "-c"
87
+ args << container
88
+ end
89
+
90
+ io = IO.popen(args, "r")
91
+
92
+ Signal.trap("INT") { io.close rescue nil; exit 0 }
93
+
94
+ begin
95
+ io.each_line do |line|
96
+ puts highlight_line(line.chomp)
97
+ end
98
+ rescue Errno::EPIPE
99
+ # Clean exit on broken pipe
100
+ ensure
101
+ io.close rescue nil
102
+ end
103
+ end
104
+
105
+ def highlight_line(line)
106
+ color_enabled = @ui.instance_variable_get(:@color)
107
+ return line unless color_enabled
108
+
109
+ if line =~ /\b(ERROR|FATAL|PANIC)\b/
110
+ "\e[31m#{line}\e[0m"
111
+ elsif line =~ /\b(WARN|WARNING)\b/
112
+ "\e[33m#{line}\e[0m"
113
+ else
114
+ line
115
+ end
116
+ end
117
+
118
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
119
+ if deploy_name
120
+ deploy_dir = File.join(kdep_dir, deploy_name)
121
+ unless File.directory?(deploy_dir)
122
+ @ui.error("Deploy target not found: #{deploy_name}")
123
+ deploys = discovery.find_deploys
124
+ if deploys.any?
125
+ @ui.info("Available deploys: #{deploys.join(', ')}")
126
+ end
127
+ return nil
128
+ end
129
+ deploy_dir
130
+ else
131
+ deploys = discovery.find_deploys
132
+ if deploys.length == 1
133
+ File.join(kdep_dir, deploys[0])
134
+ elsif deploys.length > 1
135
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
136
+ nil
137
+ else
138
+ @ui.error("No deploy targets found in kdep/")
139
+ nil
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,94 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Push
6
+ def self.option_parser
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: kdep push [deploy] [env]"
9
+ opts.separator ""
10
+ opts.separator "Pushes a built Docker image to the configured registry."
11
+ opts.separator ""
12
+ end
13
+ end
14
+
15
+ def initialize(global_options:, command_options:, args:)
16
+ @global_options = global_options
17
+ @command_options = command_options
18
+ @args = args
19
+ @ui = Kdep::UI.new(color: false)
20
+ end
21
+
22
+ def execute
23
+ deploy_name = @args[0]
24
+ env = @args[1]
25
+
26
+ # Discover kdep/ directory
27
+ discovery = Kdep::Discovery.new
28
+ kdep_dir = discovery.find_kdep_dir
29
+
30
+ unless kdep_dir
31
+ @ui.error("No kdep/ directory found")
32
+ exit 1
33
+ end
34
+
35
+ # Resolve deploy directory
36
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
37
+ unless deploy_dir
38
+ exit 1
39
+ end
40
+
41
+ # Load config
42
+ config = Kdep::Config.new(deploy_dir, env).load
43
+
44
+ # Construct full image tag
45
+ image = config["image"] || config["name"]
46
+ tag = config["tag"] || "latest"
47
+ registry = config["registry"]
48
+
49
+ if registry && !registry.to_s.empty?
50
+ full_tag = "#{registry}/#{image}:#{tag}"
51
+ else
52
+ full_tag = "#{image}:#{tag}"
53
+ end
54
+
55
+ # Push
56
+ begin
57
+ Kdep::Docker.push(full_tag)
58
+ @ui.info("Pushed: #{full_tag}")
59
+ rescue Kdep::Docker::Error => e
60
+ @ui.error(e.message)
61
+ exit 1
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
68
+ if deploy_name
69
+ deploy_dir = File.join(kdep_dir, deploy_name)
70
+ unless File.directory?(deploy_dir)
71
+ @ui.error("Deploy target not found: #{deploy_name}")
72
+ deploys = discovery.find_deploys
73
+ if deploys.any?
74
+ @ui.info("Available deploys: #{deploys.join(', ')}")
75
+ end
76
+ return nil
77
+ end
78
+ deploy_dir
79
+ else
80
+ deploys = discovery.find_deploys
81
+ if deploys.length == 1
82
+ File.join(kdep_dir, deploys[0])
83
+ elsif deploys.length > 1
84
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
85
+ nil
86
+ else
87
+ @ui.error("No deploy targets found in kdep/")
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,130 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Render
6
+ def self.option_parser
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: kdep render [deploy] [env]"
9
+ opts.separator ""
10
+ opts.separator "Renders Kubernetes manifests for the specified deploy target and environment."
11
+ end
12
+ end
13
+
14
+ def initialize(global_options:, command_options:, args:)
15
+ @global_options = global_options
16
+ @command_options = command_options
17
+ @args = args
18
+ @ui = Kdep::UI.new(color: false)
19
+ end
20
+
21
+ def execute
22
+ deploy_name = @args[0]
23
+ env = @args[1]
24
+
25
+ # Discover kdep/ directory
26
+ discovery = Kdep::Discovery.new
27
+ kdep_dir = discovery.find_kdep_dir
28
+
29
+ unless kdep_dir
30
+ @ui.error("No kdep/ directory found")
31
+ exit 1
32
+ end
33
+
34
+ # Resolve deploy directory
35
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
36
+ unless deploy_dir
37
+ exit 1
38
+ end
39
+
40
+ # Load config
41
+ config = Kdep::Config.new(deploy_dir, env).load
42
+
43
+ # Load preset resources
44
+ preset = Kdep::Preset.new(config["preset"], deploy_dir)
45
+ resources = preset.resources
46
+
47
+ # Determine output directory (.rendered/ in repo root)
48
+ repo_root = File.dirname(kdep_dir)
49
+ output_dir = File.join(repo_root, ".rendered")
50
+
51
+ # Set up writer and renderer
52
+ writer = Kdep::Writer.new(output_dir)
53
+ writer.clean
54
+ renderer = Kdep::Renderer.new(config, deploy_dir)
55
+ validator = Kdep::Validator.new
56
+
57
+ files_written = 0
58
+ errors = []
59
+
60
+ resources.each_with_index do |resource_name, idx|
61
+ index = idx + 1
62
+
63
+ # Render
64
+ begin
65
+ content = renderer.render_resource(resource_name)
66
+ rescue => e
67
+ errors << {"resource" => resource_name, "error" => e.message}
68
+ next
69
+ end
70
+
71
+ # Validate (skip empty content like ingress with no domains)
72
+ unless content.nil? || content.strip.empty?
73
+ result = validator.validate(content, resource_name)
74
+ unless result["valid"]
75
+ result["errors"].each do |err|
76
+ errors << {"resource" => resource_name, "error" => err}
77
+ end
78
+ end
79
+ end
80
+
81
+ # Write
82
+ path = writer.write(resource_name, content, index)
83
+ if path
84
+ relative_path = path.sub(repo_root + "/", "")
85
+ @ui.file_written(relative_path)
86
+ files_written += 1
87
+ end
88
+ end
89
+
90
+ # Print errors inline
91
+ errors.each do |err|
92
+ @ui.error("#{err["resource"]}: #{err["error"]}")
93
+ end
94
+
95
+ # Print summary
96
+ @ui.summary(files_written, errors.length)
97
+
98
+ exit 1 if errors.length > 0
99
+ end
100
+
101
+ private
102
+
103
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
104
+ if deploy_name
105
+ deploy_dir = File.join(kdep_dir, deploy_name)
106
+ unless File.directory?(deploy_dir)
107
+ @ui.error("Deploy target not found: #{deploy_name}")
108
+ deploys = discovery.find_deploys
109
+ if deploys.any?
110
+ @ui.info("Available deploys: #{deploys.join(', ')}")
111
+ end
112
+ return nil
113
+ end
114
+ deploy_dir
115
+ else
116
+ deploys = discovery.find_deploys
117
+ if deploys.length == 1
118
+ File.join(kdep_dir, deploys[0])
119
+ elsif deploys.length > 1
120
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
121
+ nil
122
+ else
123
+ @ui.error("No deploy targets found in kdep/")
124
+ nil
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end