cpl 1.3.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 (67) 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 +28 -1
  9. data/CONTRIBUTING.md +32 -2
  10. data/Gemfile.lock +38 -29
  11. data/README.md +43 -17
  12. data/cpl.gemspec +2 -1
  13. data/docs/commands.md +68 -59
  14. data/docs/dns.md +6 -0
  15. data/docs/migrating.md +10 -10
  16. data/docs/tips.md +15 -3
  17. data/examples/circleci.yml +3 -3
  18. data/examples/controlplane.yml +35 -9
  19. data/lib/command/apply_template.rb +66 -18
  20. data/lib/command/base.rb +168 -27
  21. data/lib/command/build_image.rb +4 -9
  22. data/lib/command/cleanup_stale_apps.rb +1 -3
  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 +35 -2
  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/promote_app_from_upstream.rb +5 -25
  35. data/lib/command/ps.rb +1 -1
  36. data/lib/command/ps_start.rb +2 -1
  37. data/lib/command/ps_stop.rb +40 -8
  38. data/lib/command/ps_wait.rb +3 -2
  39. data/lib/command/run.rb +430 -68
  40. data/lib/command/setup_app.rb +22 -2
  41. data/lib/constants/exit_code.rb +7 -0
  42. data/lib/core/config.rb +11 -3
  43. data/lib/core/controlplane.rb +126 -47
  44. data/lib/core/controlplane_api.rb +15 -1
  45. data/lib/core/controlplane_api_cli.rb +3 -3
  46. data/lib/core/controlplane_api_direct.rb +33 -5
  47. data/lib/core/shell.rb +15 -9
  48. data/lib/cpl/version.rb +1 -1
  49. data/lib/cpl.rb +50 -9
  50. data/lib/deprecated_commands.json +2 -1
  51. data/lib/generator_templates/controlplane.yml +5 -0
  52. data/lib/generator_templates/templates/{gvc.yml → app.yml} +4 -4
  53. data/lib/generator_templates/templates/postgres.yml +1 -1
  54. data/lib/generator_templates/templates/rails.yml +1 -1
  55. data/script/check_cpln_links +3 -3
  56. data/templates/app.yml +18 -0
  57. data/templates/daily-task.yml +3 -2
  58. data/templates/rails.yml +3 -2
  59. data/templates/secrets.yml +11 -0
  60. data/templates/sidekiq.yml +3 -2
  61. metadata +38 -25
  62. data/.rspec +0 -1
  63. data/lib/command/run_cleanup.rb +0 -116
  64. data/lib/command/run_detached.rb +0 -175
  65. data/lib/core/scripts.rb +0 -34
  66. data/templates/gvc.yml +0 -13
  67. data/templates/identity.yml +0 -2
@@ -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,34 @@ 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)
309
+ end
310
+
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)
313
+ end
314
+
315
+ # identities
316
+
317
+ def fetch_identity(identity, a_gvc = gvc)
318
+ api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
319
+ end
320
+
321
+ # policies
322
+
323
+ def fetch_policy(policy)
324
+ api.fetch_policy(org: org, policy: policy)
297
325
  end
298
326
 
299
- def log_get(workload:, from:, to:)
300
- api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
327
+ def bind_identity_to_policy(identity_link, policy)
328
+ cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
329
+ perform!(cmd)
301
330
  end
302
331
 
303
332
  # apply
@@ -311,13 +340,17 @@ class Controlplane # rubocop:disable Metrics/ClassLength
311
340
 
312
341
  Shell.debug("CMD", cmd)
313
342
 
314
- result = `#{cmd}`
315
- $CHILD_STATUS.success? ? parse_apply_result(result) : false
343
+ result = Shell.cmd(cmd)
344
+ parse_apply_result(result[:output]) if result[:success]
316
345
  else
317
346
  Shell.debug("CMD", cmd)
318
347
 
319
- result = `#{cmd}`
320
- $CHILD_STATUS.success? ? parse_apply_result(result) : exit(false)
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
321
354
  end
322
355
  end
323
356
  end
@@ -361,23 +394,69 @@ class Controlplane # rubocop:disable Metrics/ClassLength
361
394
 
362
395
  private
363
396
 
364
- def perform(cmd)
365
- 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
366
414
 
367
- 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
368
423
  end
369
424
 
370
- 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
+
371
428
  Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
372
429
 
373
- system(cmd) || exit(false)
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.")
374
449
  end
375
450
 
376
451
  def perform_yaml(cmd)
377
452
  Shell.debug("CMD", cmd)
378
453
 
379
- result = `#{cmd}`
380
- $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(false)
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.")
381
460
  end
382
461
 
383
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"
@@ -106,6 +112,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
106
112
  api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
107
113
  end
108
114
 
115
+ def fetch_identity(org:, gvc:, identity:)
116
+ api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
117
+ end
118
+
119
+ def fetch_policy(org:, policy:)
120
+ api_json("/org/#{org}/policy/#{policy}", method: :get)
121
+ end
122
+
109
123
  private
110
124
 
111
125
  def fetch_query_pages(result)
@@ -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
@@ -16,6 +16,7 @@ class ControlplaneApiDirect
16
16
  # ).freeze
17
17
 
18
18
  API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
19
+ API_TOKEN_EXPIRY_SECONDS = 300
19
20
 
20
21
  class << self
21
22
  attr_accessor :trace
@@ -26,7 +27,10 @@ class ControlplaneApiDirect
26
27
  uri = URI("#{api_host(host)}#{url}")
27
28
  request = API_METHODS[method].new(uri)
28
29
  request["Content-Type"] = "application/json"
29
- request["Authorization"] = api_token
30
+
31
+ refresh_api_token if should_refresh_api_token?
32
+
33
+ request["Authorization"] = api_token[:token]
30
34
  request.body = body.to_json if body
31
35
 
32
36
  Shell.debug(method.upcase, "#{uri} #{body&.to_json}")
@@ -62,17 +66,41 @@ class ControlplaneApiDirect
62
66
  end
63
67
 
64
68
  # rubocop:disable Style/ClassVars
65
- def api_token
69
+ def api_token # rubocop:disable Metrics/MethodLength
66
70
  return @@api_token if defined?(@@api_token)
67
71
 
68
- @@api_token = ENV.fetch("CPLN_TOKEN", nil)
69
- @@api_token = `cpln profile token`.chomp if @@api_token.nil?
70
- return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
72
+ @@api_token = {
73
+ token: ENV.fetch("CPLN_TOKEN", nil),
74
+ comes_from_profile: false
75
+ }
76
+ if @@api_token[:token].nil?
77
+ @@api_token = {
78
+ token: Shell.cmd("cpln", "profile", "token")[:output].chomp,
79
+ comes_from_profile: true
80
+ }
81
+ end
82
+ return @@api_token if @@api_token[:token].match?(API_TOKEN_REGEX)
71
83
 
72
84
  raise "Unknown API token format. " \
73
85
  "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
74
86
  end
75
87
 
88
+ # Returns `true` when the token is about to expire in 5 minutes
89
+ def should_refresh_api_token?
90
+ return false unless api_token[:comes_from_profile]
91
+
92
+ payload, = JWT.decode(api_token[:token], nil, false)
93
+ difference_in_seconds = payload["exp"] - Time.now.to_i
94
+
95
+ difference_in_seconds <= API_TOKEN_EXPIRY_SECONDS
96
+ rescue JWT::DecodeError
97
+ false
98
+ end
99
+
100
+ def refresh_api_token
101
+ @@api_token[:token] = `cpln profile token`.chomp
102
+ end
103
+
76
104
  def self.reset_api_token
77
105
  remove_class_variable(:@@api_token) if defined?(@@api_token)
78
106
  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.3.0"
4
+ VERSION = "2.0.0.rc.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end
data/lib/cpl.rb CHANGED
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require "dotenv/load"
4
5
  require "cgi"
5
6
  require "json"
7
+ require "jwt"
6
8
  require "net/http"
9
+ require "open3"
7
10
  require "pathname"
8
11
  require "tempfile"
9
12
  require "thor"
10
13
  require "yaml"
11
14
 
15
+ require_relative "constants/exit_code"
16
+
12
17
  # We need to require base before all commands, since the commands inherit from it
13
18
  require_relative "command/base"
14
19
 
@@ -17,6 +22,14 @@ modules = Dir["#{__dir__}/**/*.rb"].reject do |file|
17
22
  end
18
23
  modules.sort.each { require(_1) }
19
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
+
20
33
  # Fix for https://github.com/erikhuda/thor/issues/398
21
34
  # Copied from https://github.com/rails/thor/issues/398#issuecomment-622988390
22
35
  class Thor
@@ -57,9 +70,9 @@ module Cpl
57
70
 
58
71
  @checked_cpln_version = true
59
72
 
60
- result = `cpln --version 2>/dev/null`
61
- if $CHILD_STATUS.success?
62
- data = JSON.parse(result)
73
+ result = ::Shell.cmd("cpln", "--version", capture_stderr: true)
74
+ if result[:success]
75
+ data = JSON.parse(result[:output])
63
76
 
64
77
  version = data["npm"]
65
78
  min_version = Cpl::MIN_CPLN_VERSION
@@ -77,10 +90,10 @@ module Cpl
77
90
 
78
91
  @checked_cpl_version = true
79
92
 
80
- result = `gem search ^cpl$ --remote 2>/dev/null`
81
- return unless $CHILD_STATUS.success?
93
+ result = ::Shell.cmd("gem", "search", "^cpl$", "--remote", capture_stderr: true)
94
+ return unless result[:success]
82
95
 
83
- matches = result.match(/cpl \((.+)\)/)
96
+ matches = result[:output].match(/cpl \((.+)\)/)
84
97
  return unless matches
85
98
 
86
99
  version = Cpl::VERSION
@@ -133,6 +146,9 @@ module Cpl
133
146
  ::Command::Base.all_commands.merge(deprecated_commands)
134
147
  end
135
148
 
149
+ @commands_with_required_options = []
150
+ @commands_with_extra_options = []
151
+
136
152
  all_base_commands.each do |command_key, command_class| # rubocop:disable Metrics/BlockLength
137
153
  deprecated = deprecated_commands[command_key]
138
154
 
@@ -159,12 +175,20 @@ module Cpl
159
175
  long_desc(long_description)
160
176
 
161
177
  command_options.each do |option|
162
- 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)
163
185
  end
164
186
 
165
187
  # We'll handle required options manually in `Config`
166
188
  required_options = command_options.select { |option| option[:params][:required] }.map { |option| option[:name] }
167
- 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
168
192
 
169
193
  define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength
170
194
  if deprecated
@@ -184,6 +208,8 @@ module Cpl
184
208
  end
185
209
 
186
210
  begin
211
+ Cpl::Cli.validate_options!(options, command_options)
212
+
187
213
  config = Config.new(args, options, required_options)
188
214
 
189
215
  Cpl::Cli.show_info_header(config) if with_info_header
@@ -197,6 +223,21 @@ module Cpl
197
223
  ::Shell.abort("Unable to load command: #{e.message}")
198
224
  end
199
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
+
200
241
  def self.show_info_header(config) # rubocop:disable Metrics/MethodLength
201
242
  return if @showed_info_header
202
243
 
@@ -221,5 +262,5 @@ end
221
262
  # nice Ctrl+C
222
263
  trap "INT" do
223
264
  puts
224
- exit(1)
265
+ exit(ExitCode::INTERRUPT)
225
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
  }
@@ -19,10 +19,15 @@ aliases:
19
19
  one_off_workload: rails
20
20
 
21
21
  # Workloads that are for the application itself and are using application Docker images.
22
+ # These are updated with the new image when running the `deploy-image` command,
23
+ # and are also used by the `info` and `ps:` commands in order to get all of the defined workloads.
24
+ # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
25
+ # and not be listed here.
22
26
  app_workloads:
23
27
  - rails
24
28
 
25
29
  # Additional "service type" workloads, using non-application Docker images.
30
+ # These are only used by the `info` and `ps:` commands in order to get all of the defined workloads.
26
31
  additional_workloads:
27
32
  - postgres
28
33
 
@@ -1,15 +1,15 @@
1
1
  # Template setup of the GVC, roughly corresponding to a Heroku app
2
2
  kind: gvc
3
- name: APP_GVC
3
+ name: {{APP_NAME}}
4
4
  spec:
5
5
  # For using templates for test apps, put ENV values here, stored in git repo.
6
6
  # Production apps will have values configured manually after app creation.
7
7
  env:
8
8
  - name: DATABASE_URL
9
- # Password does not matter because host postgres.APP_GVC.cpln.local can only be accessed
9
+ # Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed
10
10
  # locally within CPLN GVC, and postgres running on a CPLN workload is something only for a
11
11
  # test app that lacks persistence.
12
- value: 'postgres://the_user:the_password@postgres.APP_GVC.cpln.local:5432/APP_GVC'
12
+ value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}'
13
13
  - name: RAILS_ENV
14
14
  value: production
15
15
  - name: RAILS_SERVE_STATIC_FILES
@@ -18,4 +18,4 @@ spec:
18
18
  # Part of standard configuration
19
19
  staticPlacement:
20
20
  locationLinks:
21
- - /org/APP_ORG/location/APP_LOCATION
21
+ - {{APP_LOCATION_LINK}}