cpflow 5.3.0 → 6.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.
- checksums.yaml +4 -4
- data/.agents/agent-workflow.yml +16 -25
- data/.agents/bin/README.md +19 -9
- data/.agents/bin/docs +1 -1
- data/.agents/bin/lint +1 -1
- data/.agents/bin/setup +1 -1
- data/.agents/bin/test +1 -1
- data/.agents/bin/validate +2 -1
- data/.agents/legacy-workflow-policy.yml +11 -0
- data/.agents/shaka.md +20 -0
- data/.agents/trusted-actions.yml +12 -0
- data/.coderabbit.yaml +14 -0
- data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
- data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
- data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
- data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
- data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
- data/.github/actions/cpflow-setup-environment/action.yml +8 -6
- data/.github/dependabot.yml +10 -0
- data/.github/workflows/check_cpln_links.yml +6 -1
- data/.github/workflows/claude-code-review.yml +5 -2
- data/.github/workflows/claude.yml +6 -4
- data/.github/workflows/coderabbit-lifecycle-review.yml +29 -0
- data/.github/workflows/command_docs.yml +7 -2
- data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +12 -5
- data/.github/workflows/cpflow-delete-review-app.yml +18 -11
- data/.github/workflows/cpflow-deploy-review-app.yml +38 -17
- data/.github/workflows/cpflow-deploy-staging.yml +28 -11
- data/.github/workflows/cpflow-help-command.yml +3 -3
- data/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
- data/.github/workflows/cpflow-review-app-help.yml +1 -1
- data/.github/workflows/rspec-shared.yml +34 -26
- data/.github/workflows/rspec-specific.yml +10 -2
- data/.github/workflows/rspec.yml +95 -4
- data/.github/workflows/rubocop.yml +9 -2
- data/.github/workflows/trigger-docs-site.yml +2 -0
- data/.rubocop.yml +6 -1
- data/AGENTS.md +18 -3
- data/CHANGELOG.md +39 -1
- data/CONTRIBUTING.md +39 -2
- data/Gemfile.lock +1 -1
- data/README.md +10 -2
- data/cpflow.gemspec +3 -15
- data/docs/ai-github-flow-prompt.md +3 -3
- data/docs/ci-automation.md +284 -68
- data/docs/commands.md +38 -15
- data/docs/postgres-review-apps.md +90 -0
- data/docs/postgres.md +233 -375
- data/docs/rds-private-networking.md +12 -12
- data/docs/releasing.md +15 -4
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/sidebars.ts +1 -0
- data/examples/controlplane.yml +5 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/apply_template.rb +36 -4
- data/lib/command/base.rb +56 -3
- data/lib/command/cleanup_stale_apps.rb +28 -4
- data/lib/command/copy_image_from_upstream.rb +3 -0
- data/lib/command/delete.rb +174 -4
- data/lib/command/deploy_image.rb +44 -1
- data/lib/command/generate.rb +247 -3
- data/lib/command/generate_github_actions.rb +43 -13
- data/lib/command/run.rb +224 -34
- data/lib/command/setup_app.rb +92 -10
- data/lib/command/update_github_actions.rb +82 -43
- data/lib/core/config.rb +73 -0
- data/lib/core/controlplane.rb +167 -80
- data/lib/core/controlplane_api.rb +28 -2
- data/lib/core/controlplane_api_direct.rb +17 -0
- data/lib/core/disposable_postgres_helpers.rb +167 -0
- data/lib/core/doctor_service.rb +7 -0
- data/lib/core/github_flow_readiness/checks.rb +2 -1
- data/lib/core/github_flow_readiness_service.rb +4 -0
- data/lib/core/repo_introspection.rb +112 -2
- data/lib/core/shell.rb +35 -10
- data/lib/core/timed_command.rb +111 -0
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +1 -1
- data/lib/generator_templates/.dockerignore +16 -0
- data/lib/generator_templates/Dockerfile +9 -5
- data/lib/generator_templates/entrypoint.sh +2 -0
- data/lib/generator_templates/templates/postgres.yml +9 -0
- data/lib/generator_templates_sqlite/controlplane.yml +2 -1
- data/lib/generator_templates_sqlite/release_script.sh +1 -1
- data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
- data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
- data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
- data/lib/github_flow_templates/bin/test-cpflow-github-flow +125 -46
- data/lib/patches/hash.rb +2 -2
- data/rakelib/create_release.rake +14 -14
- data/script/check_shell_scripts +66 -0
- metadata +18 -17
data/lib/core/controlplane.rb
CHANGED
|
@@ -24,18 +24,18 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def profile_exists?(profile)
|
|
27
|
-
cmd = "cpln profile get
|
|
27
|
+
cmd = ["cpln", "profile", "get", profile, "-o", "yaml"]
|
|
28
28
|
perform_yaml!(cmd).length.positive?
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def profile_create(profile, token)
|
|
32
|
-
sensitive_data_pattern = /(?<=--token )
|
|
33
|
-
cmd = "cpln profile create
|
|
32
|
+
sensitive_data_pattern = /(?<=--token ).+\z/m
|
|
33
|
+
cmd = ["cpln", "profile", "create", profile, "--token", token]
|
|
34
34
|
perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def profile_delete(profile)
|
|
38
|
-
cmd = "cpln profile delete
|
|
38
|
+
cmd = ["cpln", "profile", "delete", profile]
|
|
39
39
|
perform!(cmd)
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -102,7 +102,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
102
102
|
build_args.each { |build_arg| cmd.push("--build-arg", build_arg) }
|
|
103
103
|
cmd << docker_context
|
|
104
104
|
|
|
105
|
-
perform!(
|
|
105
|
+
perform!(cmd)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def fetch_image_details(image)
|
|
@@ -114,22 +114,22 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
def image_login(org_name = config.org)
|
|
117
|
-
cmd = "cpln image docker-login --org
|
|
117
|
+
cmd = ["cpln", "image", "docker-login", "--org", org_name]
|
|
118
118
|
perform!(cmd, output_mode: :none)
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def image_pull(image)
|
|
122
|
-
cmd = "docker pull
|
|
122
|
+
cmd = ["docker", "pull", image]
|
|
123
123
|
perform!(cmd, output_mode: :none)
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def image_tag(old_tag, new_tag)
|
|
127
|
-
cmd = "docker tag
|
|
127
|
+
cmd = ["docker", "tag", old_tag, new_tag]
|
|
128
128
|
perform!(cmd)
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def image_push(image)
|
|
132
|
-
cmd = "docker push
|
|
132
|
+
cmd = ["docker", "push", image]
|
|
133
133
|
perform!(cmd)
|
|
134
134
|
end
|
|
135
135
|
|
|
@@ -144,7 +144,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
144
144
|
# otherwise we query for a gvc with the exact name.
|
|
145
145
|
op = config.should_app_start_with?(app_name) ? "~" : "="
|
|
146
146
|
|
|
147
|
-
cmd = "cpln gvc query --org
|
|
147
|
+
cmd = ["cpln", "gvc", "query", "--org", org, "-o", "yaml", "--prop", "name#{op}#{app_name}"]
|
|
148
148
|
perform_yaml!(cmd)
|
|
149
149
|
end
|
|
150
150
|
|
|
@@ -173,8 +173,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
173
173
|
api.workload_list_by_org(org: a_org)
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
-
def fetch_workload(workload)
|
|
177
|
-
api.workload_get(workload: workload, gvc:
|
|
176
|
+
def fetch_workload(workload, a_gvc = gvc)
|
|
177
|
+
api.workload_get(workload: workload, gvc: a_gvc, org: org)
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def fetch_workload_with_status(workload)
|
|
@@ -199,12 +199,12 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
199
199
|
end
|
|
200
200
|
private :workload_status_result
|
|
201
201
|
|
|
202
|
-
def fetch_workload!(workload)
|
|
203
|
-
workload_data = fetch_workload(workload)
|
|
202
|
+
def fetch_workload!(workload, a_gvc = gvc)
|
|
203
|
+
workload_data = fetch_workload(workload, a_gvc)
|
|
204
204
|
return workload_data if workload_data
|
|
205
205
|
|
|
206
206
|
raise "Can't find workload '#{workload}', " \
|
|
207
|
-
"please create it with 'cpflow apply-template #{workload} -a #{
|
|
207
|
+
"please create it with 'cpflow apply-template #{workload} -a #{a_gvc}'."
|
|
208
208
|
end
|
|
209
209
|
|
|
210
210
|
def query_workloads(workload, a_gvc = gvc, a_org = org, partial_workload_match: false, partial_gvc_match: nil)
|
|
@@ -216,12 +216,18 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
216
216
|
end
|
|
217
217
|
|
|
218
218
|
def fetch_workload_replicas(workload, location:)
|
|
219
|
-
cmd =
|
|
220
|
-
|
|
219
|
+
cmd = [
|
|
220
|
+
"cpln", "workload", "replica", "get", workload,
|
|
221
|
+
*gvc_org_args, "--location", location, "-o", "yaml"
|
|
222
|
+
]
|
|
223
|
+
perform_yaml(cmd, err: File::NULL)
|
|
221
224
|
end
|
|
222
225
|
|
|
223
226
|
def stop_workload_replica(workload, replica, location:)
|
|
224
|
-
cmd =
|
|
227
|
+
cmd = [
|
|
228
|
+
"cpln", "workload", "replica", "stop", workload,
|
|
229
|
+
*gvc_org_args, "--replica-name", replica, "--location", location
|
|
230
|
+
]
|
|
225
231
|
perform(cmd, output_mode: :none)
|
|
226
232
|
end
|
|
227
233
|
|
|
@@ -252,9 +258,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
252
258
|
end
|
|
253
259
|
|
|
254
260
|
def workload_set_image_ref(workload, container:, image:)
|
|
255
|
-
cmd =
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
cmd = [
|
|
262
|
+
"cpln", "workload", "update", workload,
|
|
263
|
+
"--gvc", gvc, "--org", org,
|
|
264
|
+
"--set", "spec.containers.#{container}.image=/org/#{config.org}/image/#{image}"
|
|
265
|
+
]
|
|
266
|
+
Shell.debug("CMD", Shellwords.join(cmd))
|
|
267
|
+
perform_with_output(cmd)
|
|
258
268
|
end
|
|
259
269
|
|
|
260
270
|
def set_workload_env_var(workload, container:, name:, value:)
|
|
@@ -272,11 +282,16 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
272
282
|
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
|
|
273
283
|
end
|
|
274
284
|
|
|
275
|
-
def set_workload_suspend(workload, value)
|
|
276
|
-
data = fetch_workload!(workload)
|
|
285
|
+
def set_workload_suspend(workload, value, a_gvc = gvc, missing_ok: false)
|
|
286
|
+
data = missing_ok ? fetch_workload(workload, a_gvc) : fetch_workload!(workload, a_gvc)
|
|
287
|
+
return true if missing_ok && data.nil?
|
|
288
|
+
|
|
277
289
|
data["spec"]["defaultOptions"]["suspend"] = value
|
|
278
290
|
|
|
279
|
-
api.update_workload(org: org, gvc:
|
|
291
|
+
result = api.update_workload(org: org, gvc: a_gvc, workload: workload, data: data)
|
|
292
|
+
return true if missing_ok && result.nil?
|
|
293
|
+
|
|
294
|
+
result
|
|
280
295
|
end
|
|
281
296
|
|
|
282
297
|
def workload_suspended?(workload)
|
|
@@ -285,7 +300,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
285
300
|
end
|
|
286
301
|
|
|
287
302
|
def workload_force_redeployment(workload)
|
|
288
|
-
cmd = "cpln workload force-redeployment
|
|
303
|
+
cmd = ["cpln", "workload", "force-redeployment", workload, *gvc_org_args]
|
|
289
304
|
perform!(cmd)
|
|
290
305
|
end
|
|
291
306
|
|
|
@@ -294,9 +309,9 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
294
309
|
end
|
|
295
310
|
|
|
296
311
|
def workload_connect(workload, location:, container: nil, shell: nil)
|
|
297
|
-
cmd = "cpln workload connect
|
|
298
|
-
cmd
|
|
299
|
-
cmd
|
|
312
|
+
cmd = ["cpln", "workload", "connect", workload, *gvc_org_args, "--location", location]
|
|
313
|
+
cmd.push("--container", container) if container
|
|
314
|
+
cmd.push("--shell", shell) if shell
|
|
300
315
|
perform!(cmd, output_mode: :all)
|
|
301
316
|
end
|
|
302
317
|
|
|
@@ -317,14 +332,20 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
317
332
|
f.write(job_start_yaml)
|
|
318
333
|
f.rewind
|
|
319
334
|
|
|
320
|
-
cmd =
|
|
335
|
+
cmd = [
|
|
336
|
+
"cpln", "workload", "cron", "start", workload,
|
|
337
|
+
*gvc_org_args, "--file", f.path, "--location", location, "-o", "yaml"
|
|
338
|
+
]
|
|
321
339
|
perform_yaml(cmd)
|
|
322
340
|
end
|
|
323
341
|
end
|
|
324
342
|
|
|
325
|
-
def fetch_cron_workload(workload, location:)
|
|
326
|
-
cmd =
|
|
327
|
-
|
|
343
|
+
def fetch_cron_workload(workload, location:, timeout_seconds: nil)
|
|
344
|
+
cmd = [
|
|
345
|
+
"cpln", "workload", "cron", "get", workload,
|
|
346
|
+
*gvc_org_args, "--location", location, "-o", "yaml"
|
|
347
|
+
]
|
|
348
|
+
perform_yaml(cmd, timeout_seconds: timeout_seconds)
|
|
328
349
|
end
|
|
329
350
|
|
|
330
351
|
def cron_workload_deployed_version(workload)
|
|
@@ -396,7 +417,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
396
417
|
query_parts.push("replica=\"#{replica}\"") if replica
|
|
397
418
|
query = "{#{query_parts.join(',')}}"
|
|
398
419
|
|
|
399
|
-
cmd =
|
|
420
|
+
cmd = [
|
|
421
|
+
"cpln", "logs", query, "--org", org, "-t", "-o", "raw",
|
|
422
|
+
"--limit", limit.to_s, "--since", since.to_s
|
|
423
|
+
]
|
|
400
424
|
perform!(cmd, output_mode: :all)
|
|
401
425
|
end
|
|
402
426
|
|
|
@@ -410,8 +434,24 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
410
434
|
api.fetch_secret(org: org, secret: secret)
|
|
411
435
|
end
|
|
412
436
|
|
|
413
|
-
def reveal_secret(secret)
|
|
414
|
-
api.reveal_secret(org: org, secret: secret)
|
|
437
|
+
def reveal_secret(secret, required: false)
|
|
438
|
+
api.reveal_secret(org: org, secret: secret, required: required)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def create_sensitive_secret(secret, data)
|
|
442
|
+
api.create_sensitive_secret(org: org, secret: secret, app: gvc, data: data)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def patch_sensitive_secret_data(secret, data)
|
|
446
|
+
api.patch_sensitive_secret_data(org: org, secret: secret, data: data)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def secret_access_report(secret)
|
|
450
|
+
api.secret_access_report(org: org, secret: secret)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def delete_secret(secret)
|
|
454
|
+
api.delete_secret(org: org, secret: secret)
|
|
415
455
|
end
|
|
416
456
|
|
|
417
457
|
# identities
|
|
@@ -426,6 +466,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
426
466
|
api.fetch_policy(org: org, policy: policy)
|
|
427
467
|
end
|
|
428
468
|
|
|
469
|
+
def delete_policy(policy)
|
|
470
|
+
api.delete_policy(org: org, policy: policy)
|
|
471
|
+
end
|
|
472
|
+
|
|
429
473
|
def bind_identity_to_policy(identity_link, policy)
|
|
430
474
|
cmd = [
|
|
431
475
|
"cpln", "policy", "add-binding", policy,
|
|
@@ -433,7 +477,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
433
477
|
"--identity", identity_link,
|
|
434
478
|
"--permission", "reveal"
|
|
435
479
|
]
|
|
436
|
-
perform!(
|
|
480
|
+
perform!(cmd)
|
|
437
481
|
end
|
|
438
482
|
|
|
439
483
|
def unbind_identity_from_policy(identity_link, policy, permission: "reveal")
|
|
@@ -443,33 +487,25 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
443
487
|
"--identity", identity_link,
|
|
444
488
|
"--permission", permission
|
|
445
489
|
]
|
|
446
|
-
perform!(
|
|
490
|
+
perform!(cmd)
|
|
447
491
|
end
|
|
448
492
|
|
|
449
493
|
# apply
|
|
450
|
-
def apply_template(data, wait: false) # rubocop:disable Metrics/MethodLength
|
|
494
|
+
def apply_template(data, wait: false) # rubocop:disable Metrics/MethodLength
|
|
451
495
|
Tempfile.create do |f|
|
|
452
496
|
f.write(data)
|
|
453
497
|
f.rewind
|
|
454
|
-
cmd = "cpln apply
|
|
455
|
-
cmd
|
|
456
|
-
|
|
457
|
-
|
|
498
|
+
cmd = ["cpln", "apply", *gvc_org_args, "--file", f.path]
|
|
499
|
+
cmd << "--ready" if wait && ENV.fetch("DISABLE_APPLY_READY", nil).nil?
|
|
500
|
+
spawn_options = {}
|
|
501
|
+
spawn_options[:err] = Shell.tmp_stderr if Shell.should_hide_output?
|
|
458
502
|
|
|
459
|
-
|
|
503
|
+
Shell.debug("CMD", Shellwords.join(cmd))
|
|
460
504
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
else
|
|
464
|
-
Shell.debug("CMD", cmd)
|
|
505
|
+
result = Shell.cmd(*cmd, **spawn_options)
|
|
506
|
+
return parse_apply_result(result[:output]) if result[:success]
|
|
465
507
|
|
|
466
|
-
|
|
467
|
-
if result[:success]
|
|
468
|
-
parse_apply_result(result[:output])
|
|
469
|
-
else
|
|
470
|
-
Shell.abort("Command exited with non-zero status.")
|
|
471
|
-
end
|
|
472
|
-
end
|
|
508
|
+
Shell.abort("Command exited with non-zero status.") unless Shell.tmp_stderr
|
|
473
509
|
end
|
|
474
510
|
end
|
|
475
511
|
|
|
@@ -535,23 +571,15 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
535
571
|
"or ensure that the name is correct."
|
|
536
572
|
end
|
|
537
573
|
|
|
538
|
-
# `output_mode` can be :all, :errors_only or :none.
|
|
539
|
-
#
|
|
540
|
-
|
|
541
|
-
def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
|
|
574
|
+
# `output_mode` can be :all, :errors_only or :none. Redirection is expressed as
|
|
575
|
+
# Process.spawn options so argv commands never need to pass through a shell.
|
|
576
|
+
def command_spawn_options(output_mode = nil)
|
|
542
577
|
output_mode ||= determine_command_output_mode
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
when :errors_only
|
|
549
|
-
"#{cmd} > /dev/null"
|
|
550
|
-
when :none
|
|
551
|
-
"#{cmd} > /dev/null 2>&1"
|
|
552
|
-
else
|
|
553
|
-
raise "Invalid command output mode '#{output_mode}'."
|
|
554
|
-
end
|
|
578
|
+
return {} if output_mode == :all
|
|
579
|
+
return { out: File::NULL } if output_mode == :errors_only
|
|
580
|
+
return { out: File::NULL, err: File::NULL } if output_mode == :none
|
|
581
|
+
|
|
582
|
+
raise "Invalid command output mode '#{output_mode}'."
|
|
555
583
|
end
|
|
556
584
|
|
|
557
585
|
def determine_command_output_mode
|
|
@@ -565,19 +593,22 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
565
593
|
end
|
|
566
594
|
|
|
567
595
|
def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
|
568
|
-
|
|
596
|
+
validate_argv!(cmd)
|
|
597
|
+
spawn_options = command_spawn_options(output_mode)
|
|
569
598
|
|
|
570
|
-
debug_cmd =
|
|
599
|
+
debug_cmd = Shellwords.join(cmd)
|
|
571
600
|
Shell.debug("CMD", debug_cmd, sensitive_data_pattern: sensitive_data_pattern)
|
|
572
601
|
|
|
573
|
-
kernel_system_with_pid_handling(cmd)
|
|
602
|
+
kernel_system_with_pid_handling(cmd, **spawn_options)
|
|
574
603
|
end
|
|
575
604
|
|
|
576
605
|
# NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing.
|
|
577
606
|
# Returns true on zero exit, false on non-zero exit, nil when the process was signal-killed.
|
|
578
607
|
# SystemCallError (e.g. cpln binary missing) propagates — startup checks ensure this is unreachable in practice.
|
|
579
|
-
def kernel_system_with_pid_handling(cmd)
|
|
580
|
-
|
|
608
|
+
def kernel_system_with_pid_handling(cmd, **spawn_options)
|
|
609
|
+
validate_argv!(cmd)
|
|
610
|
+
|
|
611
|
+
pid = Process.spawn(*cmd, **spawn_options)
|
|
581
612
|
$child_pids << pid # rubocop:disable Style/GlobalVars
|
|
582
613
|
|
|
583
614
|
_, status = Process.wait2(pid)
|
|
@@ -586,15 +617,65 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
586
617
|
status.exited? ? status.success? : nil
|
|
587
618
|
end
|
|
588
619
|
|
|
620
|
+
def perform_with_output(cmd)
|
|
621
|
+
readers, writers = 2.times.map { IO.pipe }.transpose
|
|
622
|
+
pid = spawn_captured_process(cmd, writers)
|
|
623
|
+
captured_output = drain_captured_output(readers, determine_command_output_mode)
|
|
624
|
+
_, status = Process.wait2(pid)
|
|
625
|
+
reaped = true
|
|
626
|
+
{ output: captured_output, success: status.exited? && status.success? }
|
|
627
|
+
ensure
|
|
628
|
+
[*readers, *writers].compact.each { |pipe| pipe.close unless pipe.closed? }
|
|
629
|
+
$child_pids.delete(pid) if reaped # rubocop:disable Style/GlobalVars
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def spawn_captured_process(cmd, writers)
|
|
633
|
+
validate_argv!(cmd)
|
|
634
|
+
pid = Process.spawn(*cmd, out: writers.fetch(0), err: writers.fetch(1))
|
|
635
|
+
$child_pids << pid # rubocop:disable Style/GlobalVars
|
|
636
|
+
writers.each(&:close)
|
|
637
|
+
pid
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
def drain_captured_output(readers, output_mode)
|
|
641
|
+
stream_roles = { readers.fetch(0) => :stdout, readers.fetch(1) => :stderr }
|
|
642
|
+
pending_readers = readers.dup
|
|
643
|
+
captured_output = +""
|
|
644
|
+
|
|
645
|
+
until pending_readers.empty?
|
|
646
|
+
IO.select(pending_readers).first.each do |reader|
|
|
647
|
+
drain_ready_output(reader, stream_roles.fetch(reader), pending_readers, captured_output, output_mode)
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
captured_output
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def drain_ready_output(reader, stream_role, pending_readers, captured_output, output_mode)
|
|
655
|
+
chunk = reader.read_nonblock(4096)
|
|
656
|
+
captured_output << chunk
|
|
657
|
+
return if output_mode == :none || (stream_role == :stdout && output_mode != :all)
|
|
658
|
+
|
|
659
|
+
stream = stream_role == :stdout ? $stdout : $stderr
|
|
660
|
+
stream.write(chunk)
|
|
661
|
+
stream.flush
|
|
662
|
+
rescue IO::WaitReadable
|
|
663
|
+
nil
|
|
664
|
+
rescue EOFError
|
|
665
|
+
pending_readers.delete(reader)
|
|
666
|
+
end
|
|
667
|
+
private :spawn_captured_process, :drain_captured_output, :drain_ready_output
|
|
668
|
+
|
|
589
669
|
def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
|
590
670
|
success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
|
|
591
671
|
success || Shell.abort("Command exited with non-zero status.")
|
|
592
672
|
end
|
|
593
673
|
|
|
594
|
-
def perform_yaml(cmd)
|
|
595
|
-
|
|
674
|
+
def perform_yaml(cmd, timeout_seconds: nil, **spawn_options)
|
|
675
|
+
validate_argv!(cmd)
|
|
676
|
+
Shell.debug("CMD", Shellwords.join(cmd))
|
|
596
677
|
|
|
597
|
-
result = Shell.cmd(cmd)
|
|
678
|
+
result = Shell.cmd(*cmd, timeout_seconds: timeout_seconds, **spawn_options)
|
|
598
679
|
YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
|
|
599
680
|
end
|
|
600
681
|
|
|
@@ -602,7 +683,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
602
683
|
perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
|
|
603
684
|
end
|
|
604
685
|
|
|
605
|
-
def
|
|
606
|
-
"--gvc
|
|
686
|
+
def gvc_org_args
|
|
687
|
+
["--gvc", gvc, "--org", org]
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def validate_argv!(cmd)
|
|
691
|
+
return if cmd.is_a?(Array) && cmd.length >= 2
|
|
692
|
+
|
|
693
|
+
raise ArgumentError, "Commands must be argv arrays with at least two elements."
|
|
607
694
|
end
|
|
608
695
|
end
|
|
@@ -122,14 +122,40 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
|
122
122
|
api_json("/org/#{org}/secret/#{secret}", method: :get)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def
|
|
125
|
+
def create_sensitive_secret(org:, secret:, app:, data:)
|
|
126
|
+
api_json(
|
|
127
|
+
"/org/#{org}/secret", method: :post,
|
|
128
|
+
body: { kind: "secret", name: secret, type: "dictionary", data: data,
|
|
129
|
+
tags: { Config::GENERATED_REVIEW_APP_TAG => app } },
|
|
130
|
+
request_policy: ControlplaneApiDirect::SENSITIVE_MUTATION_REQUEST_POLICY
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def patch_sensitive_secret_data(org:, secret:, data:)
|
|
135
|
+
api_json(
|
|
136
|
+
"/org/#{org}/secret/#{secret}", method: :patch,
|
|
137
|
+
body: { data: data },
|
|
138
|
+
request_policy: ControlplaneApiDirect::SENSITIVE_MUTATION_REQUEST_POLICY
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def reveal_secret(org:, secret:, required: false)
|
|
143
|
+
request_policy = if required
|
|
144
|
+
ControlplaneApiDirect::REQUIRED_SENSITIVE_READ_REQUEST_POLICY
|
|
145
|
+
else
|
|
146
|
+
ControlplaneApiDirect::BEST_EFFORT_SENSITIVE_REQUEST_POLICY
|
|
147
|
+
end
|
|
126
148
|
api_json(
|
|
127
149
|
"/org/#{org}/secret/#{secret}/-reveal",
|
|
128
150
|
method: :get,
|
|
129
|
-
request_policy:
|
|
151
|
+
request_policy: request_policy
|
|
130
152
|
)
|
|
131
153
|
end
|
|
132
154
|
|
|
155
|
+
def secret_access_report(org:, secret:)
|
|
156
|
+
api_json("/org/#{org}/secret/#{secret}/-accessreport", method: :get)
|
|
157
|
+
end
|
|
158
|
+
|
|
133
159
|
def delete_secret(org:, secret:)
|
|
134
160
|
api_json("/org/#{org}/secret/#{secret}", method: :delete)
|
|
135
161
|
end
|
|
@@ -75,6 +75,16 @@ class ControlplaneApiDirect # rubocop:disable Metrics/ClassLength
|
|
|
75
75
|
retry_transient: false,
|
|
76
76
|
timeout: BEST_EFFORT_TIMEOUT_SECONDS
|
|
77
77
|
)
|
|
78
|
+
REQUIRED_SENSITIVE_READ_REQUEST_POLICY = RequestPolicy.new(
|
|
79
|
+
sensitive: true,
|
|
80
|
+
retry_transient: true,
|
|
81
|
+
timeout: 30
|
|
82
|
+
)
|
|
83
|
+
SENSITIVE_MUTATION_REQUEST_POLICY = RequestPolicy.new(
|
|
84
|
+
sensitive: true,
|
|
85
|
+
retry_transient: false,
|
|
86
|
+
timeout: 30
|
|
87
|
+
)
|
|
78
88
|
|
|
79
89
|
# Thread-safe API token cache. A single instance is shared process-wide by
|
|
80
90
|
# default (see `default_token_provider`) so each `ControlplaneApiDirect.new`
|
|
@@ -328,6 +338,7 @@ class ControlplaneApiDirect # rubocop:disable Metrics/ClassLength
|
|
|
328
338
|
def handle_response(response, url, request_policy)
|
|
329
339
|
case response
|
|
330
340
|
when Net::HTTPOK then parse_response_body(response, request_policy)
|
|
341
|
+
when Net::HTTPCreated then parse_created_response_body(response, request_policy)
|
|
331
342
|
when Net::HTTPAccepted then true
|
|
332
343
|
when Net::HTTPNotFound then nil
|
|
333
344
|
when Net::HTTPForbidden then raise(ForbiddenError.new(url: url, response: response))
|
|
@@ -343,6 +354,12 @@ class ControlplaneApiDirect # rubocop:disable Metrics/ClassLength
|
|
|
343
354
|
raise JSON::ParserError, "Control Plane API returned invalid JSON for a sensitive request.", cause: nil
|
|
344
355
|
end
|
|
345
356
|
|
|
357
|
+
def parse_created_response_body(response, request_policy)
|
|
358
|
+
return true if response.body.to_s.empty?
|
|
359
|
+
|
|
360
|
+
parse_response_body(response, request_policy)
|
|
361
|
+
end
|
|
362
|
+
|
|
346
363
|
def response_error_message(response, request_policy)
|
|
347
364
|
return response.to_s if request_policy.sensitive
|
|
348
365
|
|