cpflow 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/check_cpln_links.yml +19 -0
  3. data/.github/workflows/command_docs.yml +24 -0
  4. data/.github/workflows/rspec-shared.yml +56 -0
  5. data/.github/workflows/rspec.yml +28 -0
  6. data/.github/workflows/rubocop.yml +24 -0
  7. data/.gitignore +18 -0
  8. data/.overcommit.yml +16 -0
  9. data/.rubocop.yml +22 -0
  10. data/.simplecov_spawn.rb +10 -0
  11. data/CHANGELOG.md +259 -0
  12. data/CONTRIBUTING.md +73 -0
  13. data/Gemfile +7 -0
  14. data/Gemfile.lock +126 -0
  15. data/LICENSE +21 -0
  16. data/README.md +546 -0
  17. data/Rakefile +21 -0
  18. data/bin/cpflow +6 -0
  19. data/cpflow +6 -0
  20. data/cpflow.gemspec +41 -0
  21. data/docs/assets/grafana-alert.png +0 -0
  22. data/docs/assets/memcached.png +0 -0
  23. data/docs/assets/sidekiq-pre-stop-hook.png +0 -0
  24. data/docs/commands.md +454 -0
  25. data/docs/dns.md +15 -0
  26. data/docs/migrating.md +262 -0
  27. data/docs/postgres.md +436 -0
  28. data/docs/redis.md +128 -0
  29. data/docs/secrets-and-env-values.md +42 -0
  30. data/docs/tips.md +150 -0
  31. data/docs/troubleshooting.md +6 -0
  32. data/examples/circleci.yml +104 -0
  33. data/examples/controlplane.yml +159 -0
  34. data/lib/command/apply_template.rb +209 -0
  35. data/lib/command/base.rb +540 -0
  36. data/lib/command/build_image.rb +49 -0
  37. data/lib/command/cleanup_images.rb +136 -0
  38. data/lib/command/cleanup_stale_apps.rb +79 -0
  39. data/lib/command/config.rb +48 -0
  40. data/lib/command/copy_image_from_upstream.rb +108 -0
  41. data/lib/command/delete.rb +149 -0
  42. data/lib/command/deploy_image.rb +56 -0
  43. data/lib/command/doctor.rb +47 -0
  44. data/lib/command/env.rb +22 -0
  45. data/lib/command/exists.rb +23 -0
  46. data/lib/command/generate.rb +45 -0
  47. data/lib/command/info.rb +222 -0
  48. data/lib/command/latest_image.rb +19 -0
  49. data/lib/command/logs.rb +49 -0
  50. data/lib/command/maintenance.rb +42 -0
  51. data/lib/command/maintenance_off.rb +62 -0
  52. data/lib/command/maintenance_on.rb +62 -0
  53. data/lib/command/maintenance_set_page.rb +34 -0
  54. data/lib/command/no_command.rb +23 -0
  55. data/lib/command/open.rb +33 -0
  56. data/lib/command/open_console.rb +26 -0
  57. data/lib/command/promote_app_from_upstream.rb +38 -0
  58. data/lib/command/ps.rb +41 -0
  59. data/lib/command/ps_restart.rb +37 -0
  60. data/lib/command/ps_start.rb +51 -0
  61. data/lib/command/ps_stop.rb +82 -0
  62. data/lib/command/ps_wait.rb +40 -0
  63. data/lib/command/run.rb +573 -0
  64. data/lib/command/setup_app.rb +113 -0
  65. data/lib/command/test.rb +23 -0
  66. data/lib/command/version.rb +18 -0
  67. data/lib/constants/exit_code.rb +7 -0
  68. data/lib/core/config.rb +316 -0
  69. data/lib/core/controlplane.rb +552 -0
  70. data/lib/core/controlplane_api.rb +170 -0
  71. data/lib/core/controlplane_api_direct.rb +112 -0
  72. data/lib/core/doctor_service.rb +104 -0
  73. data/lib/core/helpers.rb +26 -0
  74. data/lib/core/shell.rb +100 -0
  75. data/lib/core/template_parser.rb +76 -0
  76. data/lib/cpflow/version.rb +6 -0
  77. data/lib/cpflow.rb +288 -0
  78. data/lib/deprecated_commands.json +9 -0
  79. data/lib/generator_templates/Dockerfile +27 -0
  80. data/lib/generator_templates/controlplane.yml +62 -0
  81. data/lib/generator_templates/entrypoint.sh +8 -0
  82. data/lib/generator_templates/templates/app.yml +21 -0
  83. data/lib/generator_templates/templates/postgres.yml +176 -0
  84. data/lib/generator_templates/templates/rails.yml +36 -0
  85. data/rakelib/create_release.rake +81 -0
  86. data/script/add_command +37 -0
  87. data/script/check_command_docs +3 -0
  88. data/script/check_cpln_links +45 -0
  89. data/script/rename_command +43 -0
  90. data/script/update_command_docs +62 -0
  91. data/templates/app.yml +13 -0
  92. data/templates/daily-task.yml +32 -0
  93. data/templates/maintenance.yml +25 -0
  94. data/templates/memcached.yml +24 -0
  95. data/templates/postgres.yml +32 -0
  96. data/templates/rails.yml +27 -0
  97. data/templates/redis.yml +21 -0
  98. data/templates/redis2.yml +37 -0
  99. data/templates/sidekiq.yml +38 -0
  100. metadata +341 -0
@@ -0,0 +1,552 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Controlplane # rubocop:disable Metrics/ClassLength
4
+ attr_reader :config, :api, :gvc, :org
5
+
6
+ NO_IMAGE_AVAILABLE = "NO_IMAGE_AVAILABLE"
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @api = ControlplaneApi.new
11
+ @gvc = config.app
12
+ @org = config.org
13
+
14
+ ensure_org_exists! if org
15
+ end
16
+
17
+ # profile
18
+
19
+ def profile_switch(profile)
20
+ ENV["CPLN_PROFILE"] = profile
21
+ ControlplaneApiDirect.reset_api_token
22
+ end
23
+
24
+ def profile_exists?(profile)
25
+ cmd = "cpln profile get #{profile} -o yaml"
26
+ perform_yaml!(cmd).length.positive?
27
+ end
28
+
29
+ def profile_create(profile, token)
30
+ sensitive_data_pattern = /(?<=--token )(\S+)/
31
+ cmd = "cpln profile create #{profile} --token #{token}"
32
+ perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
33
+ end
34
+
35
+ def profile_delete(profile)
36
+ cmd = "cpln profile delete #{profile}"
37
+ perform!(cmd)
38
+ end
39
+
40
+ # image
41
+
42
+ def latest_image(a_gvc = gvc, a_org = org)
43
+ @latest_image ||= {}
44
+ @latest_image[a_gvc] ||=
45
+ begin
46
+ items = query_images(a_gvc, a_org)["items"]
47
+ latest_image_from(items, app_name: a_gvc)
48
+ end
49
+ end
50
+
51
+ def latest_image_next(a_gvc = gvc, a_org = org, commit: nil)
52
+ commit ||= config.options[:commit]
53
+
54
+ @latest_image_next ||= {}
55
+ @latest_image_next[a_gvc] ||= begin
56
+ latest_image_name = latest_image(a_gvc, a_org)
57
+ image = latest_image_name.split(":").first
58
+ image += ":#{extract_image_number(latest_image_name) + 1}"
59
+ image += "_#{commit}" if commit
60
+ image
61
+ end
62
+ end
63
+
64
+ def latest_image_from(items, app_name: gvc, name_only: true)
65
+ matching_items = items.select { |item| item["name"].start_with?("#{app_name}:") }
66
+
67
+ # Or special string to indicate no image available
68
+ if matching_items.empty?
69
+ name_only ? "#{app_name}:#{NO_IMAGE_AVAILABLE}" : nil
70
+ else
71
+ latest_item = matching_items.max_by { |item| DateTime.parse(item["created"]) }
72
+ name_only ? latest_item["name"] : latest_item
73
+ end
74
+ end
75
+
76
+ def extract_image_number(image_name)
77
+ return 0 if image_name.end_with?(NO_IMAGE_AVAILABLE)
78
+
79
+ image_name.match(/:(\d+)/)&.captures&.first.to_i
80
+ end
81
+
82
+ def extract_image_commit(image_name)
83
+ image_name.match(/_(\h+)$/)&.captures&.first
84
+ end
85
+
86
+ def query_images(a_gvc = gvc, a_org = org, partial_gvc_match: nil)
87
+ partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
88
+ gvc_op = partial_gvc_match ? "~" : "="
89
+
90
+ api.query_images(org: a_org, gvc: a_gvc, gvc_op_type: gvc_op)
91
+ end
92
+
93
+ def image_build(image, dockerfile:, docker_args: [], build_args: [], push: true)
94
+ # https://docs.controlplane.com/guides/push-image#step-2
95
+ # Might need to use `docker buildx build` if compatiblitity issues arise
96
+ cmd = "docker build --platform=linux/amd64 -t #{image} -f #{dockerfile}"
97
+ cmd += " --progress=plain" if ControlplaneApiDirect.trace
98
+
99
+ cmd += " #{docker_args.join(' ')}" if docker_args.any?
100
+ build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
101
+ cmd += " #{config.app_dir}"
102
+ perform!(cmd)
103
+
104
+ image_push(image) if push
105
+ end
106
+
107
+ def fetch_image_details(image)
108
+ api.fetch_image_details(org: org, image: image)
109
+ end
110
+
111
+ def image_delete(image)
112
+ api.image_delete(org: org, image: image)
113
+ end
114
+
115
+ def image_login(org_name = config.org)
116
+ cmd = "cpln image docker-login --org #{org_name}"
117
+ perform!(cmd, output_mode: :none)
118
+ end
119
+
120
+ def image_pull(image)
121
+ cmd = "docker pull #{image}"
122
+ perform!(cmd, output_mode: :none)
123
+ end
124
+
125
+ def image_tag(old_tag, new_tag)
126
+ cmd = "docker tag #{old_tag} #{new_tag}"
127
+ perform!(cmd)
128
+ end
129
+
130
+ def image_push(image)
131
+ cmd = "docker push #{image}"
132
+ perform!(cmd)
133
+ end
134
+
135
+ # gvc
136
+
137
+ def fetch_gvcs
138
+ api.gvc_list(org: org)
139
+ end
140
+
141
+ def gvc_query(app_name = config.app)
142
+ # When `match_if_app_name_starts_with` is `true`, we query for any gvc containing the name,
143
+ # otherwise we query for a gvc with the exact name.
144
+ op = config.should_app_start_with?(app_name) ? "~" : "="
145
+
146
+ cmd = "cpln gvc query --org #{org} -o yaml --prop name#{op}#{app_name}"
147
+ perform_yaml!(cmd)
148
+ end
149
+
150
+ def fetch_gvc(a_gvc = gvc, a_org = org)
151
+ api.gvc_get(gvc: a_gvc, org: a_org)
152
+ end
153
+
154
+ def fetch_gvc!(a_gvc = gvc)
155
+ gvc_data = fetch_gvc(a_gvc)
156
+ return gvc_data if gvc_data
157
+
158
+ raise "Can't find app '#{gvc}', please create it with 'cpflow setup-app -a #{config.app}'."
159
+ end
160
+
161
+ def gvc_delete(a_gvc = gvc)
162
+ api.gvc_delete(gvc: a_gvc, org: org)
163
+ end
164
+
165
+ # workload
166
+
167
+ def fetch_workloads(a_gvc = gvc)
168
+ api.workload_list(gvc: a_gvc, org: org)
169
+ end
170
+
171
+ def fetch_workloads_by_org(a_org = org)
172
+ api.workload_list_by_org(org: a_org)
173
+ end
174
+
175
+ def fetch_workload(workload)
176
+ api.workload_get(workload: workload, gvc: gvc, org: org)
177
+ end
178
+
179
+ def fetch_workload!(workload)
180
+ workload_data = fetch_workload(workload)
181
+ return workload_data if workload_data
182
+
183
+ raise "Can't find workload '#{workload}', " \
184
+ "please create it with 'cpflow apply-template #{workload} -a #{config.app}'."
185
+ end
186
+
187
+ def query_workloads(workload, a_gvc = gvc, a_org = org, partial_workload_match: false, partial_gvc_match: nil)
188
+ partial_gvc_match = config.should_app_start_with?(a_gvc) if partial_gvc_match.nil?
189
+ gvc_op = partial_gvc_match ? "~" : "="
190
+ workload_op = partial_workload_match ? "~" : "="
191
+
192
+ api.query_workloads(org: a_org, gvc: a_gvc, workload: workload, gvc_op_type: gvc_op, workload_op_type: workload_op)
193
+ end
194
+
195
+ def fetch_workload_replicas(workload, location:)
196
+ cmd = "cpln workload replica get #{workload} #{gvc_org} --location #{location} -o yaml"
197
+ perform_yaml(cmd)
198
+ end
199
+
200
+ def stop_workload_replica(workload, replica, location:)
201
+ cmd = "cpln workload replica stop #{workload} #{gvc_org} --replica-name #{replica} --location #{location}"
202
+ perform(cmd, output_mode: :none)
203
+ end
204
+
205
+ def fetch_workload_deployments(workload)
206
+ api.workload_deployments(workload: workload, gvc: gvc, org: org)
207
+ end
208
+
209
+ def workload_deployment_version_ready?(version, next_version)
210
+ return false unless version["workload"] == next_version
211
+
212
+ version["containers"]&.all? do |_, container|
213
+ container.dig("resources", "replicas") == container.dig("resources", "replicasReady")
214
+ end
215
+ end
216
+
217
+ def workload_deployments_ready?(workload, location:, expected_status:)
218
+ deployed_replicas = fetch_workload_replicas(workload, location: location)["items"].length
219
+ return deployed_replicas.zero? if expected_status == false
220
+
221
+ deployments = fetch_workload_deployments(workload)["items"]
222
+ deployments.all? do |deployment|
223
+ next_version = deployment.dig("status", "expectedDeploymentVersion")
224
+
225
+ deployment.dig("status", "versions")&.all? do |version|
226
+ workload_deployment_version_ready?(version, next_version)
227
+ end
228
+ end
229
+ end
230
+
231
+ def workload_set_image_ref(workload, container:, image:)
232
+ cmd = "cpln workload update #{workload} #{gvc_org}"
233
+ cmd += " --set spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
234
+ perform!(cmd)
235
+ end
236
+
237
+ def set_workload_env_var(workload, container:, name:, value:)
238
+ data = fetch_workload!(workload)
239
+ data["spec"]["containers"].each do |container_data|
240
+ next unless container_data["name"] == container
241
+
242
+ container_data["env"].each do |env_data|
243
+ next unless env_data["name"] == name
244
+
245
+ env_data["value"] = value
246
+ end
247
+ end
248
+
249
+ api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
250
+ end
251
+
252
+ def set_workload_suspend(workload, value)
253
+ data = fetch_workload!(workload)
254
+ data["spec"]["defaultOptions"]["suspend"] = value
255
+
256
+ api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
257
+ end
258
+
259
+ def workload_suspended?(workload)
260
+ details = fetch_workload!(workload)
261
+ details["spec"]["defaultOptions"]["suspend"]
262
+ end
263
+
264
+ def workload_force_redeployment(workload)
265
+ cmd = "cpln workload force-redeployment #{workload} #{gvc_org}"
266
+ perform!(cmd)
267
+ end
268
+
269
+ def delete_workload(workload, a_gvc = gvc)
270
+ api.delete_workload(org: org, gvc: a_gvc, workload: workload)
271
+ end
272
+
273
+ def workload_connect(workload, location:, container: nil, shell: nil)
274
+ cmd = "cpln workload connect #{workload} #{gvc_org} --location #{location}"
275
+ cmd += " --container #{container}" if container
276
+ cmd += " --shell #{shell}" if shell
277
+ perform!(cmd, output_mode: :all)
278
+ end
279
+
280
+ def workload_exec(workload, replica, location:, container: nil, command: nil)
281
+ cmd = "cpln workload exec #{workload} #{gvc_org} --replica #{replica} --location #{location}"
282
+ cmd += " --container #{container}" if container
283
+ cmd += " -- #{command}"
284
+ perform!(cmd, output_mode: :all)
285
+ end
286
+
287
+ def start_cron_workload(workload, job_start_yaml, location:)
288
+ Tempfile.create do |f|
289
+ f.write(job_start_yaml)
290
+ f.rewind
291
+
292
+ cmd = "cpln workload cron start #{workload} #{gvc_org} --file #{f.path} --location #{location} -o yaml"
293
+ perform_yaml(cmd)
294
+ end
295
+ end
296
+
297
+ def fetch_cron_workload(workload, location:)
298
+ cmd = "cpln workload cron get #{workload} #{gvc_org} --location #{location} -o yaml"
299
+ perform_yaml(cmd)
300
+ end
301
+
302
+ def cron_workload_deployed_version(workload)
303
+ current_deployment = fetch_workload_deployments(workload)&.dig("items")&.first
304
+ return nil unless current_deployment
305
+
306
+ ready = current_deployment.dig("status", "ready")
307
+ last_processed_version = current_deployment.dig("status", "lastProcessedVersion")
308
+
309
+ ready ? last_processed_version : nil
310
+ end
311
+
312
+ # volumeset
313
+
314
+ def fetch_volumesets(a_gvc = gvc)
315
+ api.list_volumesets(org: org, gvc: a_gvc)
316
+ end
317
+
318
+ def delete_volumeset(volumeset, a_gvc = gvc)
319
+ api.delete_volumeset(org: org, gvc: a_gvc, volumeset: volumeset)
320
+ end
321
+
322
+ # domain
323
+
324
+ def find_domain_route(data)
325
+ port = data["spec"]["ports"].find { |current_port| current_port["number"] == 80 || current_port["number"] == 443 }
326
+ return nil if port.nil? || port["routes"].nil?
327
+
328
+ route = port["routes"].find { |current_route| current_route["prefix"] == "/" }
329
+ return nil if route.nil?
330
+
331
+ route
332
+ end
333
+
334
+ def find_domain_for(workloads)
335
+ domains = api.list_domains(org: org)["items"]
336
+ domains.find do |domain_data|
337
+ route = find_domain_route(domain_data)
338
+ next false if route.nil?
339
+
340
+ workloads.any? { |workload| route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}}) }
341
+ end
342
+ end
343
+
344
+ def fetch_domain(domain)
345
+ domain_data = api.fetch_domain(org: org, domain: domain)
346
+ route = find_domain_route(domain_data)
347
+ return nil if route.nil?
348
+
349
+ domain_data
350
+ end
351
+
352
+ def domain_workload_matches?(data, workload)
353
+ route = find_domain_route(data)
354
+ route["workloadLink"].match?(%r{/org/#{org}/gvc/#{gvc}/workload/#{workload}})
355
+ end
356
+
357
+ def set_domain_workload(data, workload)
358
+ route = find_domain_route(data)
359
+ route["workloadLink"] = "/org/#{org}/gvc/#{gvc}/workload/#{workload}"
360
+
361
+ api.update_domain(org: org, domain: data["name"], data: data)
362
+ end
363
+
364
+ # logs
365
+
366
+ def logs(workload:, limit:, since:, replica: nil)
367
+ query_parts = ["gvc=\"#{gvc}\"", "workload=\"#{workload}\""]
368
+ query_parts.push("replica=\"#{replica}\"") if replica
369
+ query = "{#{query_parts.join(',')}}"
370
+
371
+ cmd = "cpln logs '#{query}' --org #{org} -t -o raw --limit #{limit} --since #{since}"
372
+ perform!(cmd, output_mode: :all)
373
+ end
374
+
375
+ def log_get(workload:, from:, to:, replica: nil)
376
+ api.log_get(org: org, gvc: gvc, workload: workload, replica: replica, from: from, to: to)
377
+ end
378
+
379
+ # secrets
380
+
381
+ def fetch_secret(secret)
382
+ api.fetch_secret(org: org, secret: secret)
383
+ end
384
+
385
+ # identities
386
+
387
+ def fetch_identity(identity, a_gvc = gvc)
388
+ api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
389
+ end
390
+
391
+ # policies
392
+
393
+ def fetch_policy(policy)
394
+ api.fetch_policy(org: org, policy: policy)
395
+ end
396
+
397
+ def bind_identity_to_policy(identity_link, policy)
398
+ cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
399
+ perform!(cmd)
400
+ end
401
+
402
+ def unbind_identity_from_policy(identity_link, policy)
403
+ cmd = "cpln policy remove-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
404
+ perform!(cmd)
405
+ end
406
+
407
+ # apply
408
+ def apply_template(data) # rubocop:disable Metrics/MethodLength
409
+ Tempfile.create do |f|
410
+ f.write(data)
411
+ f.rewind
412
+ cmd = "cpln apply #{gvc_org} --file #{f.path}"
413
+ if Shell.tmp_stderr
414
+ cmd += " 2> #{Shell.tmp_stderr.path}" if Shell.should_hide_output?
415
+
416
+ Shell.debug("CMD", cmd)
417
+
418
+ result = Shell.cmd(cmd)
419
+ parse_apply_result(result[:output]) if result[:success]
420
+ else
421
+ Shell.debug("CMD", cmd)
422
+
423
+ result = Shell.cmd(cmd)
424
+ if result[:success]
425
+ parse_apply_result(result[:output])
426
+ else
427
+ Shell.abort("Command exited with non-zero status.")
428
+ end
429
+ end
430
+ end
431
+ end
432
+
433
+ def apply_hash(data)
434
+ apply_template(data.to_yaml)
435
+ end
436
+
437
+ def parse_apply_result(result) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
438
+ items = []
439
+
440
+ lines = result.split("\n")
441
+ lines.each do |line|
442
+ # The line can be in one of these formats:
443
+ # - "Created /org/shakacode-open-source-examples/gvc/my-app-staging"
444
+ # - "Created /org/shakacode-open-source-examples/gvc/my-app-staging/workload/redis"
445
+ # - "Updated gvc 'tutorial-app-test-1'"
446
+ # - "Updated workload 'redis'"
447
+ if line.start_with?("Created")
448
+ matches = line.match(%r{Created\s/org/[^/]+/gvc/([^/]+)($|(/([^/]+)/([^/]+)$))})&.captures
449
+ next unless matches
450
+
451
+ app, _, __, kind, name = matches
452
+ if kind
453
+ items.push({ kind: kind, name: name })
454
+ else
455
+ items.push({ kind: "app", name: app })
456
+ end
457
+ else
458
+ matches = line.match(/Updated\s([^\s]+)\s'([^\s]+)'$/)&.captures
459
+ next unless matches
460
+
461
+ kind, name = matches
462
+ kind = "app" if kind == "gvc"
463
+ items.push({ kind: kind, name: name })
464
+ end
465
+ end
466
+
467
+ items
468
+ end
469
+
470
+ private
471
+
472
+ def org_exists?
473
+ items = api.list_orgs["items"]
474
+ items.any? { |item| item["name"] == org }
475
+ end
476
+
477
+ def ensure_org_exists!
478
+ return if org_exists?
479
+
480
+ raise "Can't find org '#{org}', please create it in the Control Plane dashboard " \
481
+ "or ensure that the name is correct."
482
+ end
483
+
484
+ # `output_mode` can be :all, :errors_only or :none.
485
+ # If not provided, it will be determined based on the `HIDE_COMMAND_OUTPUT` env var
486
+ # or the return value of `Shell.should_hide_output?`.
487
+ def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
488
+ output_mode ||= determine_command_output_mode
489
+
490
+ case output_mode
491
+ when :all
492
+ cmd
493
+ when :errors_only
494
+ "#{cmd} > /dev/null"
495
+ when :none
496
+ "#{cmd} > /dev/null 2>&1"
497
+ else
498
+ raise "Invalid command output mode '#{output_mode}'."
499
+ end
500
+ end
501
+
502
+ def determine_command_output_mode
503
+ if ENV.fetch("HIDE_COMMAND_OUTPUT", nil) == "true"
504
+ :none
505
+ elsif Shell.should_hide_output?
506
+ :errors_only
507
+ else
508
+ :all
509
+ end
510
+ end
511
+
512
+ def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
513
+ cmd = build_command(cmd, output_mode: output_mode)
514
+
515
+ Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
516
+
517
+ kernel_system_with_pid_handling(cmd)
518
+ end
519
+
520
+ # NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing
521
+ def kernel_system_with_pid_handling(cmd)
522
+ pid = Process.spawn(cmd)
523
+ $child_pids << pid # rubocop:disable Style/GlobalVars
524
+
525
+ _, status = Process.wait2(pid)
526
+ $child_pids.delete(pid) # rubocop:disable Style/GlobalVars
527
+
528
+ status.exited? ? status.success? : nil
529
+ rescue SystemCallError
530
+ nil
531
+ end
532
+
533
+ def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
534
+ success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
535
+ success || Shell.abort("Command exited with non-zero status.")
536
+ end
537
+
538
+ def perform_yaml(cmd)
539
+ Shell.debug("CMD", cmd)
540
+
541
+ result = Shell.cmd(cmd)
542
+ YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
543
+ end
544
+
545
+ def perform_yaml!(cmd)
546
+ perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
547
+ end
548
+
549
+ def gvc_org
550
+ "--gvc #{gvc} --org #{org}"
551
+ end
552
+ end