cpflow 3.0.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 (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