cpl 1.4.0 → 2.0.0.rc.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/command_docs.yml +1 -1
  3. data/.github/workflows/rspec-shared.yml +56 -0
  4. data/.github/workflows/rspec.yml +19 -31
  5. data/.github/workflows/rubocop.yml +2 -10
  6. data/.gitignore +2 -0
  7. data/.simplecov_spawn.rb +10 -0
  8. data/CHANGELOG.md +8 -0
  9. data/CONTRIBUTING.md +32 -2
  10. data/Gemfile.lock +34 -29
  11. data/README.md +34 -25
  12. data/cpl.gemspec +1 -1
  13. data/docs/commands.md +54 -54
  14. data/docs/dns.md +6 -0
  15. data/docs/migrating.md +10 -10
  16. data/docs/tips.md +12 -10
  17. data/examples/circleci.yml +3 -3
  18. data/examples/controlplane.yml +25 -16
  19. data/lib/command/apply_template.rb +9 -9
  20. data/lib/command/base.rb +132 -37
  21. data/lib/command/build_image.rb +4 -9
  22. data/lib/command/cleanup_stale_apps.rb +1 -1
  23. data/lib/command/copy_image_from_upstream.rb +0 -7
  24. data/lib/command/delete.rb +39 -7
  25. data/lib/command/deploy_image.rb +18 -3
  26. data/lib/command/exists.rb +1 -1
  27. data/lib/command/generate.rb +1 -1
  28. data/lib/command/info.rb +7 -3
  29. data/lib/command/logs.rb +22 -2
  30. data/lib/command/maintenance_off.rb +1 -1
  31. data/lib/command/maintenance_on.rb +1 -1
  32. data/lib/command/open.rb +2 -2
  33. data/lib/command/open_console.rb +2 -2
  34. data/lib/command/ps.rb +1 -1
  35. data/lib/command/ps_start.rb +2 -1
  36. data/lib/command/ps_stop.rb +40 -8
  37. data/lib/command/ps_wait.rb +3 -2
  38. data/lib/command/run.rb +430 -69
  39. data/lib/command/setup_app.rb +4 -1
  40. data/lib/constants/exit_code.rb +7 -0
  41. data/lib/core/config.rb +1 -1
  42. data/lib/core/controlplane.rb +109 -48
  43. data/lib/core/controlplane_api.rb +7 -1
  44. data/lib/core/controlplane_api_cli.rb +3 -3
  45. data/lib/core/controlplane_api_direct.rb +1 -1
  46. data/lib/core/shell.rb +15 -9
  47. data/lib/cpl/version.rb +1 -1
  48. data/lib/cpl.rb +48 -9
  49. data/lib/deprecated_commands.json +2 -1
  50. data/lib/generator_templates/controlplane.yml +2 -2
  51. data/script/check_cpln_links +3 -3
  52. data/templates/{gvc.yml → app.yml} +5 -0
  53. data/templates/secrets.yml +8 -0
  54. metadata +23 -26
  55. data/.rspec +0 -1
  56. data/lib/command/run_cleanup.rb +0 -116
  57. data/lib/command/run_detached.rb +0 -176
  58. data/lib/core/scripts.rb +0 -34
  59. data/templates/identity.yml +0 -3
  60. data/templates/secrets-policy.yml +0 -4
  61. /data/lib/generator_templates/templates/{gvc.yml → app.yml} +0 -0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExitCode
4
+ SUCCESS = 0
5
+ ERROR_DEFAULT = 64
6
+ INTERRUPT = 130
7
+ end
data/lib/core/config.rb CHANGED
@@ -174,7 +174,7 @@ class Config # rubocop:disable Metrics/ClassLength
174
174
  end
175
175
 
176
176
  def config_file_path # rubocop:disable Metrics/MethodLength
177
- @config_file_path ||= begin
177
+ @config_file_path ||= ENV["CONFIG_FILE_PATH"] || begin
178
178
  path = Pathname.new(".").expand_path
179
179
 
180
180
  loop do
@@ -19,19 +19,17 @@ class Controlplane # rubocop:disable Metrics/ClassLength
19
19
 
20
20
  def profile_exists?(profile)
21
21
  cmd = "cpln profile get #{profile} -o yaml"
22
- perform_yaml(cmd).length.positive?
22
+ perform_yaml!(cmd).length.positive?
23
23
  end
24
24
 
25
25
  def profile_create(profile, token)
26
26
  sensitive_data_pattern = /(?<=--token )(\S+)/
27
27
  cmd = "cpln profile create #{profile} --token #{token}"
28
- cmd += " > /dev/null" if Shell.should_hide_output?
29
28
  perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
30
29
  end
31
30
 
32
31
  def profile_delete(profile)
33
32
  cmd = "cpln profile delete #{profile}"
34
- cmd += " > /dev/null" if Shell.should_hide_output?
35
33
  perform!(cmd)
36
34
  end
37
35
 
@@ -58,31 +56,31 @@ class Controlplane # rubocop:disable Metrics/ClassLength
58
56
  image_push(image) if push
59
57
  end
60
58
 
59
+ def fetch_image_details(image)
60
+ api.fetch_image_details(org: org, image: image)
61
+ end
62
+
61
63
  def image_delete(image)
62
64
  api.image_delete(org: org, image: image)
63
65
  end
64
66
 
65
67
  def image_login(org_name = config.org)
66
68
  cmd = "cpln image docker-login --org #{org_name}"
67
- cmd += " > /dev/null 2>&1" if Shell.should_hide_output?
68
- perform!(cmd)
69
+ perform!(cmd, output_mode: :none)
69
70
  end
70
71
 
71
72
  def image_pull(image)
72
73
  cmd = "docker pull #{image}"
73
- cmd += " > /dev/null" if Shell.should_hide_output?
74
- perform!(cmd)
74
+ perform!(cmd, output_mode: :none)
75
75
  end
76
76
 
77
77
  def image_tag(old_tag, new_tag)
78
78
  cmd = "docker tag #{old_tag} #{new_tag}"
79
- cmd += " > /dev/null" if Shell.should_hide_output?
80
79
  perform!(cmd)
81
80
  end
82
81
 
83
82
  def image_push(image)
84
83
  cmd = "docker push #{image}"
85
- cmd += " > /dev/null" if Shell.should_hide_output?
86
84
  perform!(cmd)
87
85
  end
88
86
 
@@ -98,7 +96,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
98
96
  op = config.should_app_start_with?(app_name) ? "~" : "="
99
97
 
100
98
  cmd = "cpln gvc query --org #{org} -o yaml --prop name#{op}#{app_name}"
101
- perform_yaml(cmd)
99
+ perform_yaml!(cmd)
102
100
  end
103
101
 
104
102
  def fetch_gvc(a_gvc = gvc, a_org = org)
@@ -145,41 +143,38 @@ class Controlplane # rubocop:disable Metrics/ClassLength
145
143
  api.query_workloads(org: a_org, gvc: a_gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
146
144
  end
147
145
 
148
- def workload_get_replicas(workload, location:)
149
- cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml"
146
+ def fetch_workload_replicas(workload, location:)
147
+ cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
150
148
  perform_yaml(cmd)
151
149
  end
152
150
 
153
- def workload_get_replicas_safely(workload, location:)
154
- cmd = "cpln workload get-replicas #{workload} #{gvc_org} --location #{location} -o yaml"
155
- cmd += " 2> /dev/null" if Shell.should_hide_output?
156
-
157
- Shell.debug("CMD", cmd)
158
-
159
- result = `#{cmd}`
160
- $CHILD_STATUS.success? ? YAML.safe_load(result) : nil
151
+ def stop_workload_replica(workload, replica, location:)
152
+ cmd = "cpln workload replica stop #{workload} #{gvc_org} --replica-name #{replica} --location #{location}"
153
+ perform(cmd, output_mode: :none)
161
154
  end
162
155
 
163
156
  def fetch_workload_deployments(workload)
164
157
  api.workload_deployments(workload: workload, gvc: gvc, org: org)
165
158
  end
166
159
 
167
- def workload_deployment_version_ready?(version, next_version, expected_status:)
160
+ def workload_deployment_version_ready?(version, next_version)
168
161
  return false unless version["workload"] == next_version
169
162
 
170
163
  version["containers"]&.all? do |_, container|
171
- ready = container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
172
- expected_status == true ? ready : !ready
164
+ container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
173
165
  end
174
166
  end
175
167
 
176
- def workload_deployments_ready?(workload, expected_status:)
168
+ def workload_deployments_ready?(workload, location:, expected_status:)
169
+ deployed_replicas = fetch_workload_replicas(workload, location: location)["items"].length
170
+ return deployed_replicas.zero? if expected_status == false
171
+
177
172
  deployments = fetch_workload_deployments(workload)["items"]
178
173
  deployments.all? do |deployment|
179
174
  next_version = deployment.dig("status", "expectedDeploymentVersion")
180
175
 
181
176
  deployment.dig("status", "versions")&.all? do |version|
182
- workload_deployment_version_ready?(version, next_version, expected_status: expected_status)
177
+ workload_deployment_version_ready?(version, next_version)
183
178
  end
184
179
  end
185
180
  end
@@ -187,7 +182,6 @@ class Controlplane # rubocop:disable Metrics/ClassLength
187
182
  def workload_set_image_ref(workload, container:, image:)
188
183
  cmd = "cpln workload update #{workload} #{gvc_org}"
189
184
  cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
190
- cmd += " > /dev/null" if Shell.should_hide_output?
191
185
  perform!(cmd)
192
186
  end
193
187
 
@@ -215,7 +209,6 @@ class Controlplane # rubocop:disable Metrics/ClassLength
215
209
 
216
210
  def workload_force_redeployment(workload)
217
211
  cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
218
- cmd += " > /dev/null" if Shell.should_hide_output?
219
212
  perform!(cmd)
220
213
  end
221
214
 
@@ -227,14 +220,29 @@ class Controlplane # rubocop:disable Metrics/ClassLength
227
220
  cmd = "cpln workload connect #{workload} #{gvc_org} --location #{location}"
228
221
  cmd += " --container #{container}" if container
229
222
  cmd += " --shell #{shell}" if shell
230
- perform!(cmd)
223
+ perform!(cmd, output_mode: :all)
231
224
  end
232
225
 
233
- def workload_exec(workload, location:, container: nil, command: nil)
234
- cmd = "cpln workload exec #{workload} #{gvc_org} --location #{location}"
226
+ def workload_exec(workload, replica, location:, container: nil, command: nil)
227
+ cmd = "cpln workload exec #{workload} #{gvc_org} --replica #{replica} --location #{location}"
235
228
  cmd += " --container #{container}" if container
236
229
  cmd += " -- #{command}"
237
- perform!(cmd)
230
+ perform!(cmd, output_mode: :all)
231
+ end
232
+
233
+ def start_cron_workload(workload, job_start_yaml, location:)
234
+ Tempfile.create do |f|
235
+ f.write(job_start_yaml)
236
+ f.rewind
237
+
238
+ cmd = "cpln workload cron start #{workload} #{gvc_org} --file #{f.path} --location #{location} -o yaml"
239
+ perform_yaml(cmd)
240
+ end
241
+ end
242
+
243
+ def fetch_cron_workload(workload, location:)
244
+ cmd = "cpln workload cron get #{workload} #{gvc_org} --location #{location} -o yaml"
245
+ perform_yaml(cmd)
238
246
  end
239
247
 
240
248
  # volumeset
@@ -291,13 +299,17 @@ class Controlplane # rubocop:disable Metrics/ClassLength
291
299
 
292
300
  # logs
293
301
 
294
- def logs(workload:)
295
- cmd = "cpln logs '{workload=\"#{workload}\"}' --org #{org} -t -o raw --limit 200"
296
- perform!(cmd)
302
+ def logs(workload:, limit:, since:, replica: nil)
303
+ query_parts = ["gvc=\"#{gvc}\"", "workload=\"#{workload}\""]
304
+ query_parts.push("replica=\"#{replica}\"") if replica
305
+ query = "{#{query_parts.join(',')}}"
306
+
307
+ cmd = "cpln logs '#{query}' --org #{org} -t -o raw --limit #{limit} --since #{since}"
308
+ perform!(cmd, output_mode: :all)
297
309
  end
298
310
 
299
- def log_get(workload:, from:, to:)
300
- api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
311
+ def log_get(workload:, from:, to:, replica: nil)
312
+ api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
301
313
  end
302
314
 
303
315
  # identities
@@ -314,7 +326,6 @@ class Controlplane # rubocop:disable Metrics/ClassLength
314
326
 
315
327
  def bind_identity_to_policy(identity_link, policy)
316
328
  cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
317
- cmd += " > /dev/null" if Shell.should_hide_output?
318
329
  perform!(cmd)
319
330
  end
320
331
 
@@ -329,13 +340,17 @@ class Controlplane # rubocop:disable Metrics/ClassLength
329
340
 
330
341
  Shell.debug("CMD", cmd)
331
342
 
332
- result = `#{cmd}`
333
- $CHILD_STATUS.success? ? parse_apply_result(result) : false
343
+ result = Shell.cmd(cmd)
344
+ parse_apply_result(result[:output]) if result[:success]
334
345
  else
335
346
  Shell.debug("CMD", cmd)
336
347
 
337
- result = `#{cmd}`
338
- $CHILD_STATUS.success? ? parse_apply_result(result) : exit(1)
348
+ result = Shell.cmd(cmd)
349
+ if result[:success]
350
+ parse_apply_result(result[:output])
351
+ else
352
+ Shell.abort("Command exited with non-zero status.")
353
+ end
339
354
  end
340
355
  end
341
356
  end
@@ -379,23 +394,69 @@ class Controlplane # rubocop:disable Metrics/ClassLength
379
394
 
380
395
  private
381
396
 
382
- def perform(cmd)
383
- Shell.debug("CMD", cmd)
397
+ # `output_mode` can be :all, :errors_only or :none.
398
+ # If not provided, it will be determined based on the `HIDE_COMMAND_OUTPUT` env var
399
+ # or the return value of `Shell.should_hide_output?`.
400
+ def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
401
+ output_mode ||= determine_command_output_mode
402
+
403
+ case output_mode
404
+ when :all
405
+ cmd
406
+ when :errors_only
407
+ "#{cmd} > /dev/null"
408
+ when :none
409
+ "#{cmd} > /dev/null 2>&1"
410
+ else
411
+ raise "Invalid command output mode '#{output_mode}'."
412
+ end
413
+ end
384
414
 
385
- system(cmd)
415
+ def determine_command_output_mode
416
+ if ENV.fetch("HIDE_COMMAND_OUTPUT", nil) == "true"
417
+ :none
418
+ elsif Shell.should_hide_output?
419
+ :errors_only
420
+ else
421
+ :all
422
+ end
386
423
  end
387
424
 
388
- def perform!(cmd, sensitive_data_pattern: nil)
425
+ def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
426
+ cmd = build_command(cmd, output_mode: output_mode)
427
+
389
428
  Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
390
429
 
391
- system(cmd) || exit(1)
430
+ kernel_system_with_pid_handling(cmd)
431
+ end
432
+
433
+ # NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing
434
+ def kernel_system_with_pid_handling(cmd)
435
+ pid = Process.spawn(cmd)
436
+ $child_pids << pid # rubocop:disable Style/GlobalVars
437
+
438
+ _, status = Process.wait2(pid)
439
+ $child_pids.delete(pid) # rubocop:disable Style/GlobalVars
440
+
441
+ status.exited? ? status.success? : nil
442
+ rescue SystemCallError
443
+ nil
444
+ end
445
+
446
+ def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
447
+ success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
448
+ success || Shell.abort("Command exited with non-zero status.")
392
449
  end
393
450
 
394
451
  def perform_yaml(cmd)
395
452
  Shell.debug("CMD", cmd)
396
453
 
397
- result = `#{cmd}`
398
- $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(1)
454
+ result = Shell.cmd(cmd)
455
+ YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
456
+ end
457
+
458
+ def perform_yaml!(cmd)
459
+ perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
399
460
  end
400
461
 
401
462
  def gvc_org
@@ -25,18 +25,24 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
25
25
  query("/org/#{org}/image", terms)
26
26
  end
27
27
 
28
+ def fetch_image_details(org:, image:)
29
+ api_json("/org/#{org}/image/#{image}", method: :get)
30
+ end
31
+
28
32
  def image_delete(org:, image:)
29
33
  api_json("/org/#{org}/image/#{image}", method: :delete)
30
34
  end
31
35
 
32
- def log_get(org:, gvc:, workload: nil, from: nil, to: nil)
36
+ def log_get(org:, gvc:, workload: nil, replica: nil, from: nil, to: nil) # rubocop:disable Metrics/ParameterLists
33
37
  query = { gvc: gvc }
34
38
  query[:workload] = workload if workload
39
+ query[:replica] = replica if replica
35
40
  query = query.map { |k, v| %(#{k}="#{v}") }.join(",").then { "{#{_1}}" }
36
41
 
37
42
  params = { query: query }
38
43
  params[:from] = "#{from}000000000" if from
39
44
  params[:to] = "#{to}000000000" if to
45
+ params[:limit] = "5000"
40
46
  # params << "delay_for=0"
41
47
  # params << "limit=30"
42
48
  # params << "direction=forward"
@@ -2,9 +2,9 @@
2
2
 
3
3
  class ControlplaneApiCli
4
4
  def call(url, method:)
5
- response = `cpln rest #{method} #{url} -o json`
6
- raise(response) unless $CHILD_STATUS.success?
5
+ result = Shell.cmd("cpln", "rest", method, url, "-o", "json", capture_stderr: true)
6
+ raise(result[:output]) unless result[:success]
7
7
 
8
- JSON.parse(response)
8
+ JSON.parse(result[:output])
9
9
  end
10
10
  end
@@ -75,7 +75,7 @@ class ControlplaneApiDirect
75
75
  }
76
76
  if @@api_token[:token].nil?
77
77
  @@api_token = {
78
- token: `cpln profile token`.chomp,
78
+ token: Shell.cmd("cpln", "profile", "token")[:output].chomp,
79
79
  comes_from_profile: true
80
80
  }
81
81
  end
data/lib/core/shell.rb CHANGED
@@ -9,10 +9,6 @@ class Shell
9
9
  @shell ||= Thor::Shell::Color.new
10
10
  end
11
11
 
12
- def self.stderr
13
- @stderr ||= $stderr
14
- end
15
-
16
12
  def self.use_tmp_stderr
17
13
  @tmp_stderr = Tempfile.create
18
14
 
@@ -40,15 +36,16 @@ class Shell
40
36
  end
41
37
 
42
38
  def self.warn(message)
43
- stderr.puts(color("WARNING: #{message}", :yellow))
39
+ Kernel.warn(color("WARNING: #{message}", :yellow))
44
40
  end
45
41
 
46
42
  def self.warn_deprecated(message)
47
- stderr.puts(color("DEPRECATED: #{message}", :yellow))
43
+ Kernel.warn(color("DEPRECATED: #{message}", :yellow))
48
44
  end
49
45
 
50
- def self.abort(message)
51
- Kernel.abort(color("ERROR: #{message}", :red))
46
+ def self.abort(message, exit_status = ExitCode::ERROR_DEFAULT)
47
+ Kernel.warn(color("ERROR: #{message}", :red))
48
+ exit(exit_status)
52
49
  end
53
50
 
54
51
  def self.verbose_mode(verbose)
@@ -59,13 +56,22 @@ class Shell
59
56
  return unless verbose
60
57
 
61
58
  filtered_message = hide_sensitive_data(message, sensitive_data_pattern)
62
- stderr.puts("\n[#{color(prefix, :red)}] #{filtered_message}")
59
+ Kernel.warn("\n[#{color(prefix, :red)}] #{filtered_message}")
63
60
  end
64
61
 
65
62
  def self.should_hide_output?
66
63
  tmp_stderr && !verbose
67
64
  end
68
65
 
66
+ def self.cmd(*cmd_to_run, capture_stderr: false)
67
+ output, status = capture_stderr ? Open3.capture2e(*cmd_to_run) : Open3.capture2(*cmd_to_run)
68
+
69
+ {
70
+ output: output,
71
+ success: status.success?
72
+ }
73
+ end
74
+
69
75
  #
70
76
  # Hide sensitive data based on the passed pattern
71
77
  #
data/lib/cpl/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "1.4.0"
4
+ VERSION = "2.0.0.rc.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end
data/lib/cpl.rb CHANGED
@@ -6,11 +6,14 @@ require "cgi"
6
6
  require "json"
7
7
  require "jwt"
8
8
  require "net/http"
9
+ require "open3"
9
10
  require "pathname"
10
11
  require "tempfile"
11
12
  require "thor"
12
13
  require "yaml"
13
14
 
15
+ require_relative "constants/exit_code"
16
+
14
17
  # We need to require base before all commands, since the commands inherit from it
15
18
  require_relative "command/base"
16
19
 
@@ -19,6 +22,14 @@ modules = Dir["#{__dir__}/**/*.rb"].reject do |file|
19
22
  end
20
23
  modules.sort.each { require(_1) }
21
24
 
25
+ # NOTE: this snippet combines all subprocesses into a group and kills all on exit to avoid hanging orphans
26
+ $child_pids = [] # rubocop:disable Style/GlobalVars
27
+ at_exit do
28
+ $child_pids.each do |pid| # rubocop:disable Style/GlobalVars
29
+ Process.kill("TERM", pid)
30
+ end
31
+ end
32
+
22
33
  # Fix for https://github.com/erikhuda/thor/issues/398
23
34
  # Copied from https://github.com/rails/thor/issues/398#issuecomment-622988390
24
35
  class Thor
@@ -59,9 +70,9 @@ module Cpl
59
70
 
60
71
  @checked_cpln_version = true
61
72
 
62
- result = `cpln --version 2>/dev/null`
63
- if $CHILD_STATUS.success?
64
- data = JSON.parse(result)
73
+ result = ::Shell.cmd("cpln", "--version", capture_stderr: true)
74
+ if result[:success]
75
+ data = JSON.parse(result[:output])
65
76
 
66
77
  version = data["npm"]
67
78
  min_version = Cpl::MIN_CPLN_VERSION
@@ -79,10 +90,10 @@ module Cpl
79
90
 
80
91
  @checked_cpl_version = true
81
92
 
82
- result = `gem search ^cpl$ --remote 2>/dev/null`
83
- return unless $CHILD_STATUS.success?
93
+ result = ::Shell.cmd("gem", "search", "^cpl$", "--remote", capture_stderr: true)
94
+ return unless result[:success]
84
95
 
85
- matches = result.match(/cpl \((.+)\)/)
96
+ matches = result[:output].match(/cpl \((.+)\)/)
86
97
  return unless matches
87
98
 
88
99
  version = Cpl::VERSION
@@ -135,6 +146,9 @@ module Cpl
135
146
  ::Command::Base.all_commands.merge(deprecated_commands)
136
147
  end
137
148
 
149
+ @commands_with_required_options = []
150
+ @commands_with_extra_options = []
151
+
138
152
  all_base_commands.each do |command_key, command_class| # rubocop:disable Metrics/BlockLength
139
153
  deprecated = deprecated_commands[command_key]
140
154
 
@@ -161,12 +175,20 @@ module Cpl
161
175
  long_desc(long_description)
162
176
 
163
177
  command_options.each do |option|
164
- method_option(option[:name], **option[:params])
178
+ params = option[:params]
179
+
180
+ # Ensures that if no value is provided for a non-boolean option (e.g., `cpl command --option`),
181
+ # it defaults to an empty string instead of the option name (which is the default Thor behavior)
182
+ params[:lazy_default] ||= "" if params[:type] != :boolean
183
+
184
+ method_option(option[:name], **params)
165
185
  end
166
186
 
167
187
  # We'll handle required options manually in `Config`
168
188
  required_options = command_options.select { |option| option[:params][:required] }.map { |option| option[:name] }
169
- disable_required_check! name_for_method.to_sym if required_options.any?
189
+ @commands_with_required_options.push(name_for_method.to_sym) if required_options.any?
190
+
191
+ @commands_with_extra_options.push(name_for_method.to_sym) if accepts_extra_options
170
192
 
171
193
  define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength
172
194
  if deprecated
@@ -186,6 +208,8 @@ module Cpl
186
208
  end
187
209
 
188
210
  begin
211
+ Cpl::Cli.validate_options!(options, command_options)
212
+
189
213
  config = Config.new(args, options, required_options)
190
214
 
191
215
  Cpl::Cli.show_info_header(config) if with_info_header
@@ -199,6 +223,21 @@ module Cpl
199
223
  ::Shell.abort("Unable to load command: #{e.message}")
200
224
  end
201
225
 
226
+ disable_required_check!(*@commands_with_required_options)
227
+ check_unknown_options!(except: @commands_with_extra_options)
228
+ stop_on_unknown_option!
229
+
230
+ def self.validate_options!(options, command_options)
231
+ options.each do |name, value|
232
+ raise "No value provided for option '#{name}'." if value.to_s.strip.empty?
233
+
234
+ params = command_options.find { |option| option[:name].to_s == name }[:params]
235
+ next unless params[:valid_regex]
236
+
237
+ raise "Invalid value provided for option '#{name}'." unless value.match?(params[:valid_regex])
238
+ end
239
+ end
240
+
202
241
  def self.show_info_header(config) # rubocop:disable Metrics/MethodLength
203
242
  return if @showed_info_header
204
243
 
@@ -223,5 +262,5 @@ end
223
262
  # nice Ctrl+C
224
263
  trap "INT" do
225
264
  puts
226
- exit(1)
265
+ exit(ExitCode::INTERRUPT)
227
266
  end
@@ -3,6 +3,7 @@
3
3
  "cleanup_old_images": "cleanup-images",
4
4
  "promote": "deploy-image",
5
5
  "promote_image": "deploy-image",
6
- "runner": "run:detached",
6
+ "run:detached": "run",
7
+ "runner": "run",
7
8
  "setup": "apply-template"
8
9
  }
@@ -20,14 +20,14 @@ aliases:
20
20
 
21
21
  # Workloads that are for the application itself and are using application Docker images.
22
22
  # These are updated with the new image when running the `deploy-image` command,
23
- # and are also used by the `info`, `ps:`, and `run:cleanup` commands in order to get all of the defined workloads.
23
+ # and are also used by the `info` and `ps:` commands in order to get all of the defined workloads.
24
24
  # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
25
25
  # and not be listed here.
26
26
  app_workloads:
27
27
  - rails
28
28
 
29
29
  # Additional "service type" workloads, using non-application Docker images.
30
- # These are only used by the `info`, `ps:` and `run:cleanup` commands in order to get all of the defined workloads.
30
+ # These are only used by the `info` and `ps:` commands in order to get all of the defined workloads.
31
31
  additional_workloads:
32
32
  - postgres
33
33
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- bad_links=("controlplane.com/shakacode")
4
- proper_links=("shakacode.controlplane.com")
3
+ bad_links=("controlplane.com/shakacode" "https://docs.controlplane.com")
4
+ proper_links=("shakacode.controlplane.com" "https://shakadocs.controlplane.com")
5
5
 
6
6
  bold=$(tput bold)
7
7
  normal=$(tput sgr0)
@@ -19,7 +19,7 @@ for ((idx = 0; idx < ${#bad_links[@]}; idx++)); do
19
19
  --heading \
20
20
  --color=always -- \
21
21
  "${bad_links[idx]}" \
22
- ':!script/check_cpln_links')
22
+ ':!script/check_cpln_links' '*.md')
23
23
 
24
24
  # Line would become really unwieldly if everything was mushed into the
25
25
  # conditional, so let's ignore this check here.
@@ -11,3 +11,8 @@ spec:
11
11
  staticPlacement:
12
12
  locationLinks:
13
13
  - {{APP_LOCATION_LINK}}
14
+ ---
15
+ # Identity is needed to access secrets
16
+ kind: identity
17
+ name: {{APP_IDENTITY}}
18
+
@@ -1,3 +1,11 @@
1
1
  kind: secret
2
2
  name: {{APP_SECRETS}}
3
3
  type: dictionary
4
+ data: {}
5
+ ---
6
+ # Policy is needed to allow identities to access secrets
7
+ kind: policy
8
+ name: {{APP_SECRETS_POLICY}}
9
+ targetKind: secret
10
+ targetLinks:
11
+ - //secret/{{APP_SECRETS}}