cpl 1.4.0 → 2.0.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
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}}