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.
- checksums.yaml +4 -4
- data/.github/workflows/command_docs.yml +1 -1
- data/.github/workflows/rspec-shared.yml +56 -0
- data/.github/workflows/rspec.yml +19 -31
- data/.github/workflows/rubocop.yml +2 -10
- data/.gitignore +2 -0
- data/.simplecov_spawn.rb +10 -0
- data/CHANGELOG.md +28 -1
- data/CONTRIBUTING.md +32 -2
- data/Gemfile.lock +38 -29
- data/README.md +43 -17
- data/cpl.gemspec +2 -1
- data/docs/commands.md +68 -59
- data/docs/dns.md +6 -0
- data/docs/migrating.md +10 -10
- data/docs/tips.md +15 -3
- data/examples/circleci.yml +3 -3
- data/examples/controlplane.yml +35 -9
- data/lib/command/apply_template.rb +66 -18
- data/lib/command/base.rb +168 -27
- data/lib/command/build_image.rb +4 -9
- data/lib/command/cleanup_stale_apps.rb +1 -3
- data/lib/command/copy_image_from_upstream.rb +0 -7
- data/lib/command/delete.rb +39 -7
- data/lib/command/deploy_image.rb +35 -2
- data/lib/command/exists.rb +1 -1
- data/lib/command/generate.rb +1 -1
- data/lib/command/info.rb +7 -3
- data/lib/command/logs.rb +22 -2
- data/lib/command/maintenance_off.rb +1 -1
- data/lib/command/maintenance_on.rb +1 -1
- data/lib/command/open.rb +2 -2
- data/lib/command/open_console.rb +2 -2
- data/lib/command/promote_app_from_upstream.rb +5 -25
- data/lib/command/ps.rb +1 -1
- data/lib/command/ps_start.rb +2 -1
- data/lib/command/ps_stop.rb +40 -8
- data/lib/command/ps_wait.rb +3 -2
- data/lib/command/run.rb +430 -68
- data/lib/command/setup_app.rb +22 -2
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +11 -3
- data/lib/core/controlplane.rb +126 -47
- data/lib/core/controlplane_api.rb +15 -1
- data/lib/core/controlplane_api_cli.rb +3 -3
- data/lib/core/controlplane_api_direct.rb +33 -5
- data/lib/core/shell.rb +15 -9
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +50 -9
- data/lib/deprecated_commands.json +2 -1
- data/lib/generator_templates/controlplane.yml +5 -0
- data/lib/generator_templates/templates/{gvc.yml → app.yml} +4 -4
- data/lib/generator_templates/templates/postgres.yml +1 -1
- data/lib/generator_templates/templates/rails.yml +1 -1
- data/script/check_cpln_links +3 -3
- data/templates/app.yml +18 -0
- data/templates/daily-task.yml +3 -2
- data/templates/rails.yml +3 -2
- data/templates/secrets.yml +11 -0
- data/templates/sidekiq.yml +3 -2
- metadata +38 -25
- data/.rspec +0 -1
- data/lib/command/run_cleanup.rb +0 -116
- data/lib/command/run_detached.rb +0 -175
- data/lib/core/scripts.rb +0 -34
- data/templates/gvc.yml +0 -13
- data/templates/identity.yml +0 -2
data/lib/core/controlplane.rb
CHANGED
@@ -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
|
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
|
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
|
149
|
-
cmd = "cpln workload get
|
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
|
154
|
-
cmd = "cpln workload
|
155
|
-
cmd
|
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
|
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
|
-
|
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
|
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
|
-
|
296
|
-
|
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
|
300
|
-
|
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 =
|
315
|
-
|
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 =
|
320
|
-
|
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
|
-
|
365
|
-
|
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
|
-
|
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
|
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
|
-
|
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 =
|
380
|
-
|
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
|
-
|
6
|
-
raise(
|
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(
|
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
|
-
|
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 =
|
69
|
-
|
70
|
-
|
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
|
-
|
39
|
+
Kernel.warn(color("WARNING: #{message}", :yellow))
|
44
40
|
end
|
45
41
|
|
46
42
|
def self.warn_deprecated(message)
|
47
|
-
|
43
|
+
Kernel.warn(color("DEPRECATED: #{message}", :yellow))
|
48
44
|
end
|
49
45
|
|
50
|
-
def self.abort(message)
|
51
|
-
Kernel.
|
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
|
-
|
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
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 =
|
61
|
-
if
|
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 =
|
81
|
-
return unless
|
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
|
-
|
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
|
-
|
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(
|
265
|
+
exit(ExitCode::INTERRUPT)
|
225
266
|
end
|
@@ -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:
|
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.
|
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.
|
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
|
-
-
|
21
|
+
- {{APP_LOCATION_LINK}}
|