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,109 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Restart
6
+ RESTARTABLE_PRESETS = %w[web worker].freeze
7
+ NON_RESTARTABLE_PRESETS = %w[job cronjob].freeze
8
+
9
+ def self.option_parser
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: kdep restart [deploy] [--type TYPE]"
12
+ opts.separator ""
13
+ opts.separator "Triggers a rolling restart of a Deployment or StatefulSet."
14
+ opts.separator ""
15
+ opts.on("--type TYPE", "Resource type: deployment, statefulset (overrides preset)") do |_|
16
+ # parsed via into: hash
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(global_options:, command_options:, args:)
22
+ @global_options = global_options
23
+ @command_options = command_options
24
+ @args = args
25
+ @ui = Kdep::UI.new(color: false)
26
+ end
27
+
28
+ def execute
29
+ deploy_name = @args[0]
30
+
31
+ # Discover kdep/ directory
32
+ discovery = Kdep::Discovery.new
33
+ kdep_dir = discovery.find_kdep_dir
34
+ unless kdep_dir
35
+ @ui.error("No kdep/ directory found")
36
+ exit 1
37
+ end
38
+
39
+ # Resolve deploy directory
40
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
41
+ unless deploy_dir
42
+ exit 1
43
+ end
44
+
45
+ # Load config
46
+ config = Kdep::Config.new(deploy_dir, nil).load
47
+
48
+ # Validate context
49
+ Kdep::ContextGuard.new(config["context"]).validate!
50
+
51
+ namespace = config["namespace"]
52
+ app_name = config["name"]
53
+ preset = config["preset"]
54
+
55
+ # Determine resource type
56
+ resource_type = determine_resource_type(preset)
57
+
58
+ # Execute rollout restart
59
+ Kdep::Kubectl.run("rollout", "restart", "#{resource_type}/#{app_name}", "-n", namespace)
60
+ @ui.info("Restarted #{resource_type}/#{app_name} in #{namespace}")
61
+ end
62
+
63
+ private
64
+
65
+ def determine_resource_type(preset)
66
+ if @command_options[:type]
67
+ return @command_options[:type]
68
+ end
69
+
70
+ resource_type_for_preset(preset)
71
+ end
72
+
73
+ def resource_type_for_preset(preset)
74
+ if NON_RESTARTABLE_PRESETS.include?(preset)
75
+ @ui.error("#{preset} workloads cannot be restarted (use --type to override)")
76
+ exit 1
77
+ end
78
+
79
+ "deployment"
80
+ end
81
+
82
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
83
+ if deploy_name
84
+ deploy_dir = File.join(kdep_dir, deploy_name)
85
+ unless File.directory?(deploy_dir)
86
+ @ui.error("Deploy target not found: #{deploy_name}")
87
+ deploys = discovery.find_deploys
88
+ if deploys.any?
89
+ @ui.info("Available deploys: #{deploys.join(', ')}")
90
+ end
91
+ return nil
92
+ end
93
+ deploy_dir
94
+ else
95
+ deploys = discovery.find_deploys
96
+ if deploys.length == 1
97
+ File.join(kdep_dir, deploys[0])
98
+ elsif deploys.length > 1
99
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
100
+ nil
101
+ else
102
+ @ui.error("No deploy targets found in kdep/")
103
+ nil
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,136 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Scale
6
+ NON_SCALABLE_PRESETS = %w[job cronjob].freeze
7
+
8
+ def self.option_parser
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: kdep scale [deploy] N [--type TYPE]"
11
+ opts.separator ""
12
+ opts.separator "Scales a Deployment or StatefulSet to N replicas."
13
+ opts.separator ""
14
+ opts.on("--type TYPE", "Resource type: deployment, statefulset (overrides preset)") do |_|
15
+ # parsed via into: hash
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(global_options:, command_options:, args:)
21
+ @global_options = global_options
22
+ @command_options = command_options
23
+ @args = args
24
+ @ui = Kdep::UI.new(color: false)
25
+ end
26
+
27
+ def execute
28
+ if @args.length == 1 && @args[0] && @args[0].match?(/\A\d+\z/)
29
+ deploy_name = nil
30
+ @replica_count = @args[0].to_i
31
+ else
32
+ deploy_name = @args[0]
33
+ end
34
+
35
+ # Discover kdep/ directory
36
+ discovery = Kdep::Discovery.new
37
+ kdep_dir = discovery.find_kdep_dir
38
+ unless kdep_dir
39
+ @ui.error("No kdep/ directory found")
40
+ exit 1
41
+ end
42
+
43
+ # Resolve deploy directory
44
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
45
+ unless deploy_dir
46
+ exit 1
47
+ end
48
+
49
+ # Load config
50
+ config = Kdep::Config.new(deploy_dir, nil).load
51
+
52
+ # Validate context
53
+ Kdep::ContextGuard.new(config["context"]).validate!
54
+
55
+ namespace = config["namespace"]
56
+ app_name = config["name"]
57
+ preset = config["preset"]
58
+
59
+ # Parse and validate replica count
60
+ replicas = parse_replicas
61
+ unless replicas
62
+ @ui.error("Usage: kdep scale [deploy] N -- replica count required")
63
+ exit 1
64
+ end
65
+
66
+ # Determine resource type
67
+ resource_type = determine_resource_type(preset)
68
+
69
+ # Execute scale
70
+ Kdep::Kubectl.run("scale", "#{resource_type}/#{app_name}", "--replicas=#{replicas}", "-n", namespace)
71
+ @ui.info("Scaled #{resource_type}/#{app_name} to #{replicas} replicas in #{namespace}")
72
+ end
73
+
74
+ private
75
+
76
+ def parse_replicas
77
+ return @replica_count if @replica_count
78
+
79
+ # Second arg should be the replica count (first arg is deploy name)
80
+ replica_str = @args[1]
81
+ return nil unless replica_str
82
+
83
+ # Must be a non-negative integer
84
+ unless replica_str.match?(/\A\d+\z/)
85
+ @ui.error("Invalid replica count: #{replica_str} (must be a non-negative integer)")
86
+ exit 1
87
+ end
88
+
89
+ replica_str.to_i
90
+ end
91
+
92
+ def determine_resource_type(preset)
93
+ if @command_options[:type]
94
+ return @command_options[:type]
95
+ end
96
+
97
+ resource_type_for_preset(preset)
98
+ end
99
+
100
+ def resource_type_for_preset(preset)
101
+ if NON_SCALABLE_PRESETS.include?(preset)
102
+ @ui.error("#{preset} workloads cannot be scaled (use --type to override)")
103
+ exit 1
104
+ end
105
+
106
+ "deployment"
107
+ end
108
+
109
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
110
+ if deploy_name
111
+ deploy_dir = File.join(kdep_dir, deploy_name)
112
+ unless File.directory?(deploy_dir)
113
+ @ui.error("Deploy target not found: #{deploy_name}")
114
+ deploys = discovery.find_deploys
115
+ if deploys.any?
116
+ @ui.info("Available deploys: #{deploys.join(', ')}")
117
+ end
118
+ return nil
119
+ end
120
+ deploy_dir
121
+ else
122
+ deploys = discovery.find_deploys
123
+ if deploys.length == 1
124
+ File.join(kdep_dir, deploys[0])
125
+ elsif deploys.length > 1
126
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
127
+ nil
128
+ else
129
+ @ui.error("No deploy targets found in kdep/")
130
+ nil
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,223 @@
1
+ require "optparse"
2
+ require "json"
3
+ require "base64"
4
+
5
+ module Kdep
6
+ module Commands
7
+ class Secrets
8
+ def self.option_parser
9
+ OptionParser.new do |opts|
10
+ opts.banner = "Usage: kdep secrets <list|create|check> [deploy]"
11
+ opts.separator ""
12
+ opts.separator "Manage image pull secrets in the deploy namespace."
13
+ opts.separator ""
14
+ opts.separator "Sub-commands:"
15
+ opts.separator " list List image pull secrets"
16
+ opts.separator " create Create a docker-registry secret"
17
+ opts.separator " check Decode and inspect an existing secret"
18
+ opts.separator ""
19
+ opts.separator "Options:"
20
+ opts.on("--server=URL", "Docker registry server URL")
21
+ opts.on("--username=NAME", "Registry username")
22
+ opts.on("--password=PASS", "Registry password")
23
+ opts.on("--name=SECRET_NAME", "Secret name (default: registry-credentials)")
24
+ end
25
+ end
26
+
27
+ def initialize(global_options:, command_options:, args:)
28
+ @global_options = global_options
29
+ @command_options = command_options
30
+ @args = args
31
+ @ui = Kdep::UI.new(color: false)
32
+ end
33
+
34
+ def execute
35
+ sub = @args.shift # Sub-command first (list, create, check)
36
+ deploy_name = @args.shift # Deploy name second (optional)
37
+
38
+ # If no sub-command given, show usage
39
+ unless sub
40
+ show_usage
41
+ return
42
+ end
43
+
44
+ # Only proceed with deploy resolution for known sub-commands
45
+ unless %w[list create check].include?(sub)
46
+ show_usage
47
+ return
48
+ end
49
+
50
+ # Discover kdep/ directory
51
+ discovery = Kdep::Discovery.new
52
+ kdep_dir = discovery.find_kdep_dir
53
+ unless kdep_dir
54
+ @ui.error("No kdep/ directory found")
55
+ exit 1
56
+ end
57
+
58
+ # Resolve deploy directory
59
+ deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
60
+ unless deploy_dir
61
+ exit 1
62
+ end
63
+
64
+ # Load config
65
+ @config = Kdep::Config.new(deploy_dir, nil).load
66
+
67
+ # Validate context
68
+ Kdep::ContextGuard.new(@config["context"]).validate!
69
+
70
+ @namespace = @config["namespace"]
71
+
72
+ # Dispatch sub-command
73
+ case sub
74
+ when "list" then list_secrets
75
+ when "create" then create_secret
76
+ when "check" then check_secrets
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def list_secrets
83
+ data = Kdep::Kubectl.run_json(
84
+ "get", "secrets", "-n", @namespace,
85
+ "--field-selector", "type=kubernetes.io/dockerconfigjson"
86
+ )
87
+
88
+ items = data["items"]
89
+ if items.nil? || items.empty?
90
+ @ui.info("No image pull secrets in #{@namespace}")
91
+ return
92
+ end
93
+
94
+ items.each do |secret|
95
+ name = secret["metadata"]["name"]
96
+ created = secret["metadata"]["creationTimestamp"]
97
+ @ui.info("#{name} created: #{created}")
98
+ end
99
+ end
100
+
101
+ def create_secret
102
+ server = @command_options[:server]
103
+ username = @command_options[:username]
104
+ password = @command_options[:password]
105
+ secret_name = @command_options[:name] || "registry-credentials"
106
+
107
+ # Default server from config registry if available
108
+ if server.nil? && @config["registry"]
109
+ # Extract hostname from registry (e.g., "ghcr.io/myorg" -> "ghcr.io")
110
+ registry = @config["registry"]
111
+ server = registry.include?("/") ? registry.split("/").first : registry
112
+ end
113
+
114
+ # Validate required flags
115
+ missing = []
116
+ missing << "--username" unless username
117
+ missing << "--password" unless password
118
+ missing << "--server" unless server
119
+
120
+ unless missing.empty?
121
+ @ui.error("Missing required flags: #{missing.join(', ')}")
122
+ @ui.info("Usage: kdep secrets create [deploy] --server=URL --username=NAME --password=PASS [--name=SECRET_NAME]")
123
+ exit 1
124
+ end
125
+
126
+ Kdep::Kubectl.run(
127
+ "create", "secret", "docker-registry", secret_name,
128
+ "--docker-server=#{server}",
129
+ "--docker-username=#{username}",
130
+ "--docker-password=#{password}",
131
+ "-n", @namespace
132
+ )
133
+
134
+ @ui.info("Created secret #{secret_name} in #{@namespace}")
135
+ end
136
+
137
+ def check_secrets
138
+ secret_name = @command_options[:name] || @args.shift
139
+
140
+ unless secret_name
141
+ # No name given; list secrets and check all found
142
+ data = Kdep::Kubectl.run_json(
143
+ "get", "secrets", "-n", @namespace,
144
+ "--field-selector", "type=kubernetes.io/dockerconfigjson"
145
+ )
146
+ items = data["items"]
147
+ if items.nil? || items.empty?
148
+ @ui.info("No image pull secrets found in #{@namespace}")
149
+ return
150
+ end
151
+ items.each do |secret|
152
+ check_single_secret(secret["metadata"]["name"])
153
+ end
154
+ return
155
+ end
156
+
157
+ check_single_secret(secret_name)
158
+ end
159
+
160
+ def check_single_secret(secret_name)
161
+ data = Kdep::Kubectl.run_json("get", "secret", secret_name, "-n", @namespace)
162
+
163
+ raw = data["data"] && data["data"][".dockerconfigjson"]
164
+ unless raw
165
+ @ui.error("Secret #{secret_name} does not contain .dockerconfigjson")
166
+ return
167
+ end
168
+
169
+ begin
170
+ decoded = Base64.decode64(raw)
171
+ config = JSON.parse(decoded)
172
+ auths = config["auths"] || {}
173
+
174
+ if auths.empty?
175
+ @ui.info("Secret #{secret_name}: no auth entries found")
176
+ return
177
+ end
178
+
179
+ auths.each_key do |server|
180
+ @ui.info("Found credentials for #{server}")
181
+ end
182
+ rescue JSON::ParserError => e
183
+ @ui.error("Failed to parse dockerconfigjson from #{secret_name}: #{e.message}")
184
+ end
185
+ end
186
+
187
+ def show_usage
188
+ @ui.info("Usage: kdep secrets <sub-command> [deploy]")
189
+ @ui.info("")
190
+ @ui.info("Sub-commands:")
191
+ @ui.info(" list List image pull secrets")
192
+ @ui.info(" create Create a docker-registry secret")
193
+ @ui.info(" check Decode and inspect an existing secret")
194
+ end
195
+
196
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
197
+ if deploy_name
198
+ deploy_dir = File.join(kdep_dir, deploy_name)
199
+ unless File.directory?(deploy_dir)
200
+ @ui.error("Deploy target not found: #{deploy_name}")
201
+ deploys = discovery.find_deploys
202
+ if deploys.any?
203
+ @ui.info("Available deploys: #{deploys.join(', ')}")
204
+ end
205
+ return nil
206
+ end
207
+ deploy_dir
208
+ else
209
+ deploys = discovery.find_deploys
210
+ if deploys.length == 1
211
+ File.join(kdep_dir, deploys[0])
212
+ elsif deploys.length > 1
213
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
214
+ nil
215
+ else
216
+ @ui.error("No deploy targets found in kdep/")
217
+ nil
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,117 @@
1
+ require "optparse"
2
+
3
+ module Kdep
4
+ module Commands
5
+ class Sh
6
+ def self.option_parser
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: kdep sh [deploy] [container]"
9
+ opts.separator ""
10
+ opts.separator "Opens an interactive shell inside a running pod."
11
+ opts.separator ""
12
+ opts.on("-c", "--container=NAME", "Container name for multi-container pods")
13
+ end
14
+ end
15
+
16
+ def initialize(global_options:, command_options:, args:)
17
+ @global_options = global_options
18
+ @command_options = command_options
19
+ @args = args
20
+ @ui = Kdep::UI.new(color: false)
21
+ end
22
+
23
+ def execute
24
+ deploy_name = @args[0]
25
+
26
+ # Discover kdep/ directory
27
+ discovery = Kdep::Discovery.new
28
+ kdep_dir = discovery.find_kdep_dir
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, nil).load
42
+
43
+ # Validate context
44
+ Kdep::ContextGuard.new(config["context"]).validate!
45
+
46
+ # Find running pod
47
+ namespace = config["namespace"]
48
+ app_name = config["name"]
49
+ pod_name = find_pod(namespace, app_name)
50
+
51
+ # Exec into pod
52
+ exec_shell(pod_name, namespace)
53
+ end
54
+
55
+ private
56
+
57
+ def find_pod(namespace, app_name)
58
+ data = Kdep::Kubectl.run_json(
59
+ "get", "pods",
60
+ "-n", namespace,
61
+ "-l", "app=#{app_name}",
62
+ "--field-selector", "status.phase=Running"
63
+ )
64
+ pods = data["items"]
65
+
66
+ if pods.nil? || pods.empty?
67
+ @ui.error("No running pods found for #{app_name} in #{namespace}")
68
+ exit 1
69
+ end
70
+
71
+ pods[0]["metadata"]["name"]
72
+ end
73
+
74
+ def exec_shell(pod_name, namespace)
75
+ container = @command_options[:container]
76
+
77
+ args = ["kubectl", "exec", "-it", pod_name, "-n", namespace]
78
+
79
+ if container
80
+ args << "-c"
81
+ args << container
82
+ end
83
+
84
+ args << "--"
85
+ args << "/bin/sh"
86
+
87
+ Kernel.exec(*args)
88
+ end
89
+
90
+ def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
91
+ if deploy_name
92
+ deploy_dir = File.join(kdep_dir, deploy_name)
93
+ unless File.directory?(deploy_dir)
94
+ @ui.error("Deploy target not found: #{deploy_name}")
95
+ deploys = discovery.find_deploys
96
+ if deploys.any?
97
+ @ui.info("Available deploys: #{deploys.join(', ')}")
98
+ end
99
+ return nil
100
+ end
101
+ deploy_dir
102
+ else
103
+ deploys = discovery.find_deploys
104
+ if deploys.length == 1
105
+ File.join(kdep_dir, deploys[0])
106
+ elsif deploys.length > 1
107
+ @ui.error("Multiple deploys found, specify one: #{deploys.join(', ')}")
108
+ nil
109
+ else
110
+ @ui.error("No deploy targets found in kdep/")
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end