cpflow 5.2.0 → 6.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/.agents/agent-workflow.yml +26 -2
- data/.agents/bin/README.md +16 -4
- 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/.coderabbit.yaml +13 -0
- data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
- data/.github/actions/cpflow-setup-environment/action.yml +9 -7
- data/.github/actions/cpflow-wait-for-health/action.yml +87 -15
- data/.github/dependabot.yml +10 -0
- data/.github/pull_request_template.md +18 -0
- data/.github/workflows/check_cpln_links.yml +6 -1
- data/.github/workflows/claude-code-review.yml +5 -2
- data/.github/workflows/claude.yml +97 -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 +638 -43
- data/.github/workflows/cpflow-deploy-review-app.yml +665 -36
- data/.github/workflows/cpflow-deploy-staging.yml +16 -10
- data/.github/workflows/cpflow-help-command.yml +3 -3
- data/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
- data/.github/workflows/cpflow-review-app-help.yml +6 -14
- data/.github/workflows/rspec-shared.yml +34 -26
- data/.github/workflows/rspec-specific.yml +10 -2
- data/.github/workflows/rspec.yml +71 -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 +6 -49
- data/CHANGELOG.md +56 -1
- data/CONTRIBUTING.md +48 -3
- data/Gemfile.lock +1 -1
- data/README.md +6 -1
- data/cpflow.gemspec +3 -15
- data/docs/ai-github-flow-prompt.md +2 -2
- data/docs/ci-automation.md +216 -99
- data/docs/commands.md +36 -9
- data/docs/rds-private-networking.md +12 -12
- data/docs/releasing.md +15 -4
- data/docs/secrets-and-env-values.md +8 -0
- data/docs/tips.md +18 -0
- data/examples/controlplane.yml +5 -0
- data/lib/command/ai_github_flow_prompt.rb +1 -1
- data/lib/command/apply_template.rb +104 -2
- data/lib/command/base.rb +52 -3
- data/lib/command/cleanup_stale_apps.rb +28 -4
- data/lib/command/copy_image_from_upstream.rb +3 -0
- data/lib/command/deploy_image.rb +54 -3
- data/lib/command/generate_github_actions.rb +36 -12
- data/lib/command/run.rb +357 -44
- data/lib/command/setup_app.rb +10 -5
- data/lib/command/update_github_actions.rb +7 -6
- data/lib/core/controlplane.rb +179 -79
- data/lib/core/controlplane_api.rb +8 -0
- data/lib/core/controlplane_api_direct.rb +257 -63
- data/lib/core/shell.rb +39 -7
- data/lib/core/timed_command.rb +111 -0
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +1 -1
- data/lib/github_flow_templates/.github/cpflow-help.md +24 -10
- data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +10 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +9 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
- data/lib/github_flow_templates/bin/test-cpflow-github-flow +63 -3
- data/lib/patches/hash.rb +2 -2
- data/rakelib/create_release.rake +14 -14
- data/script/check_shell_scripts +66 -0
- metadata +12 -18
data/lib/command/setup_app.rb
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Command
|
|
4
|
-
class SetupApp < Base
|
|
4
|
+
class SetupApp < Base # rubocop:disable Metrics/ClassLength
|
|
5
5
|
NAME = "setup-app"
|
|
6
6
|
OPTIONS = [
|
|
7
7
|
app_option(required: true),
|
|
8
8
|
skip_secret_access_binding_option,
|
|
9
9
|
skip_secrets_setup_option,
|
|
10
|
-
skip_post_creation_hook_option
|
|
10
|
+
skip_post_creation_hook_option,
|
|
11
|
+
refresh_templates_option
|
|
11
12
|
].freeze
|
|
12
13
|
DESCRIPTION = "Creates an app and all its workloads"
|
|
13
14
|
LONG_DESCRIPTION = <<~DESC
|
|
@@ -23,18 +24,21 @@ module Command
|
|
|
23
24
|
- Runs a post-creation hook after the app is created if `hooks.post_creation` is specified in the `.controlplane/controlplane.yml` file
|
|
24
25
|
- If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
|
25
26
|
- Use `--skip-post-creation-hook` to skip the hook if specified in `controlplane.yml`
|
|
27
|
+
- Use `--refresh-templates` to apply configured templates noninteractively to an existing app while preserving each workload's configured app image even when workloads are unready or use mixed image versions, skipping existing secret resources entirely, repairing secrets access bindings, and skipping the post-creation hook
|
|
26
28
|
DESC
|
|
27
29
|
VALIDATIONS = %w[config templates].freeze
|
|
28
30
|
|
|
29
|
-
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
31
|
+
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
30
32
|
templates = config[:setup_app_templates]
|
|
33
|
+
refresh_templates = config.options[:refresh_templates]
|
|
31
34
|
|
|
32
35
|
app = cp.fetch_gvc
|
|
33
|
-
if app
|
|
36
|
+
if app && !refresh_templates
|
|
34
37
|
raise "App '#{config.app}' already exists. If you want to update this app, " \
|
|
35
38
|
"either run 'cpflow delete -a #{config.app}' and then re-run this command, " \
|
|
36
39
|
"or run 'cpflow apply-template #{templates.join(' ')} -a #{config.app}'."
|
|
37
40
|
end
|
|
41
|
+
raise "App '#{config.app}' does not exist, so its templates cannot be refreshed." if !app && refresh_templates
|
|
38
42
|
|
|
39
43
|
skip_secrets_setup = skip_secrets_setup?
|
|
40
44
|
|
|
@@ -45,11 +49,12 @@ module Command
|
|
|
45
49
|
|
|
46
50
|
args = []
|
|
47
51
|
args.push("--add-app-identity") unless skip_secrets_setup
|
|
52
|
+
args.push("--yes", "--preserve-existing-runtime") if refresh_templates
|
|
48
53
|
run_cpflow_command("apply-template", *templates, "-a", config.app, *args)
|
|
49
54
|
|
|
50
55
|
bind_identity_to_policy unless skip_secrets_setup
|
|
51
56
|
bind_shared_secret_policy_grants(shared_secret_policy_grant_pairs) unless skip_secrets_setup
|
|
52
|
-
run_post_creation_hook unless config.options[:skip_post_creation_hook]
|
|
57
|
+
run_post_creation_hook unless refresh_templates || config.options[:skip_post_creation_hook]
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
private
|
|
@@ -10,12 +10,13 @@ module Command
|
|
|
10
10
|
|
|
11
11
|
NAME = "update-github-actions"
|
|
12
12
|
OPTIONS = [staging_branch_option].freeze
|
|
13
|
-
DESCRIPTION = "Regenerates
|
|
13
|
+
DESCRIPTION = "Regenerates GitHub Actions files for the installed cpflow version"
|
|
14
14
|
LONG_DESCRIPTION = <<~DESC.freeze
|
|
15
|
-
Regenerates the
|
|
16
|
-
from the currently installed cpflow gem. Use this after
|
|
17
|
-
cpflow gem so checked-in workflow wrappers move to the
|
|
18
|
-
release tag, for example `v#{Cpflow::VERSION}
|
|
15
|
+
Regenerates the cpflow workflow wrappers, local composite actions, and
|
|
16
|
+
helper files from the currently installed cpflow gem. Use this after
|
|
17
|
+
updating the cpflow gem so checked-in workflow wrappers move to the
|
|
18
|
+
matching upstream release tag, for example `v#{Cpflow::VERSION}`, and the
|
|
19
|
+
generated action implementations move with them.
|
|
19
20
|
|
|
20
21
|
If the existing generated staging workflow uses a custom single staging
|
|
21
22
|
branch, the command preserves it. Pass `--staging-branch BRANCH` to set or
|
|
@@ -23,7 +24,7 @@ module Command
|
|
|
23
24
|
DESC
|
|
24
25
|
EXAMPLES = <<~EX
|
|
25
26
|
```sh
|
|
26
|
-
# After updating the cpflow gem, refresh generated GitHub Actions
|
|
27
|
+
# After updating the cpflow gem, refresh every generated GitHub Actions file
|
|
27
28
|
cpflow update-github-actions
|
|
28
29
|
|
|
29
30
|
# When running cpflow through Bundler
|
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,16 +173,38 @@ 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
|
+
end
|
|
179
|
+
|
|
180
|
+
def fetch_workload_with_status(workload)
|
|
181
|
+
result = workload_status_result(workload)
|
|
182
|
+
|
|
183
|
+
unless result[:success]
|
|
184
|
+
Shell.warn("Failed to fetch status for '#{workload}': #{result[:error_output].to_s.strip}")
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
JSON.parse(result[:output])
|
|
189
|
+
rescue JSON::ParserError => e
|
|
190
|
+
Shell.warn("Failed to parse status for '#{workload}': #{e.message}")
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def workload_status_result(workload)
|
|
195
|
+
# The direct API response omits computed fields such as status.readyLatest.
|
|
196
|
+
args = ["cpln", "workload", "get", workload, "--gvc", gvc, "--org", org, "-o", "json"]
|
|
197
|
+
Shell.debug("CMD", Shellwords.join(args))
|
|
198
|
+
Shell.cmd(*args, separate_stderr: true)
|
|
178
199
|
end
|
|
200
|
+
private :workload_status_result
|
|
179
201
|
|
|
180
|
-
def fetch_workload!(workload)
|
|
181
|
-
workload_data = fetch_workload(workload)
|
|
202
|
+
def fetch_workload!(workload, a_gvc = gvc)
|
|
203
|
+
workload_data = fetch_workload(workload, a_gvc)
|
|
182
204
|
return workload_data if workload_data
|
|
183
205
|
|
|
184
206
|
raise "Can't find workload '#{workload}', " \
|
|
185
|
-
"please create it with 'cpflow apply-template #{workload} -a #{
|
|
207
|
+
"please create it with 'cpflow apply-template #{workload} -a #{a_gvc}'."
|
|
186
208
|
end
|
|
187
209
|
|
|
188
210
|
def query_workloads(workload, a_gvc = gvc, a_org = org, partial_workload_match: false, partial_gvc_match: nil)
|
|
@@ -194,12 +216,18 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
194
216
|
end
|
|
195
217
|
|
|
196
218
|
def fetch_workload_replicas(workload, location:)
|
|
197
|
-
cmd =
|
|
198
|
-
|
|
219
|
+
cmd = [
|
|
220
|
+
"cpln", "workload", "replica", "get", workload,
|
|
221
|
+
*gvc_org_args, "--location", location, "-o", "yaml"
|
|
222
|
+
]
|
|
223
|
+
perform_yaml(cmd, err: File::NULL)
|
|
199
224
|
end
|
|
200
225
|
|
|
201
226
|
def stop_workload_replica(workload, replica, location:)
|
|
202
|
-
cmd =
|
|
227
|
+
cmd = [
|
|
228
|
+
"cpln", "workload", "replica", "stop", workload,
|
|
229
|
+
*gvc_org_args, "--replica-name", replica, "--location", location
|
|
230
|
+
]
|
|
203
231
|
perform(cmd, output_mode: :none)
|
|
204
232
|
end
|
|
205
233
|
|
|
@@ -230,9 +258,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
230
258
|
end
|
|
231
259
|
|
|
232
260
|
def workload_set_image_ref(workload, container:, image:)
|
|
233
|
-
cmd =
|
|
234
|
-
|
|
235
|
-
|
|
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)
|
|
236
268
|
end
|
|
237
269
|
|
|
238
270
|
def set_workload_env_var(workload, container:, name:, value:)
|
|
@@ -250,11 +282,16 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
250
282
|
api.update_workload(org: org, gvc: gvc, workload: workload, data: data)
|
|
251
283
|
end
|
|
252
284
|
|
|
253
|
-
def set_workload_suspend(workload, value)
|
|
254
|
-
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
|
+
|
|
255
289
|
data["spec"]["defaultOptions"]["suspend"] = value
|
|
256
290
|
|
|
257
|
-
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
|
|
258
295
|
end
|
|
259
296
|
|
|
260
297
|
def workload_suspended?(workload)
|
|
@@ -263,7 +300,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
263
300
|
end
|
|
264
301
|
|
|
265
302
|
def workload_force_redeployment(workload)
|
|
266
|
-
cmd = "cpln workload force-redeployment
|
|
303
|
+
cmd = ["cpln", "workload", "force-redeployment", workload, *gvc_org_args]
|
|
267
304
|
perform!(cmd)
|
|
268
305
|
end
|
|
269
306
|
|
|
@@ -272,16 +309,21 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
272
309
|
end
|
|
273
310
|
|
|
274
311
|
def workload_connect(workload, location:, container: nil, shell: nil)
|
|
275
|
-
cmd = "cpln workload connect
|
|
276
|
-
cmd
|
|
277
|
-
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
|
|
278
315
|
perform!(cmd, output_mode: :all)
|
|
279
316
|
end
|
|
280
317
|
|
|
281
318
|
def workload_exec(workload, replica, location:, container: nil, command: nil)
|
|
282
|
-
cmd =
|
|
283
|
-
|
|
284
|
-
|
|
319
|
+
cmd = [
|
|
320
|
+
"cpln", "workload", "exec", workload,
|
|
321
|
+
"--gvc", gvc, "--org", org,
|
|
322
|
+
"--replica", replica, "--location", location, "-it"
|
|
323
|
+
]
|
|
324
|
+
cmd.push("--container", container) if container
|
|
325
|
+
cmd << "--"
|
|
326
|
+
cmd.concat(Array(command))
|
|
285
327
|
perform(cmd, output_mode: :all)
|
|
286
328
|
end
|
|
287
329
|
|
|
@@ -290,14 +332,20 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
290
332
|
f.write(job_start_yaml)
|
|
291
333
|
f.rewind
|
|
292
334
|
|
|
293
|
-
cmd =
|
|
335
|
+
cmd = [
|
|
336
|
+
"cpln", "workload", "cron", "start", workload,
|
|
337
|
+
*gvc_org_args, "--file", f.path, "--location", location, "-o", "yaml"
|
|
338
|
+
]
|
|
294
339
|
perform_yaml(cmd)
|
|
295
340
|
end
|
|
296
341
|
end
|
|
297
342
|
|
|
298
|
-
def fetch_cron_workload(workload, location:)
|
|
299
|
-
cmd =
|
|
300
|
-
|
|
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)
|
|
301
349
|
end
|
|
302
350
|
|
|
303
351
|
def cron_workload_deployed_version(workload)
|
|
@@ -369,7 +417,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
369
417
|
query_parts.push("replica=\"#{replica}\"") if replica
|
|
370
418
|
query = "{#{query_parts.join(',')}}"
|
|
371
419
|
|
|
372
|
-
cmd =
|
|
420
|
+
cmd = [
|
|
421
|
+
"cpln", "logs", query, "--org", org, "-t", "-o", "raw",
|
|
422
|
+
"--limit", limit.to_s, "--since", since.to_s
|
|
423
|
+
]
|
|
373
424
|
perform!(cmd, output_mode: :all)
|
|
374
425
|
end
|
|
375
426
|
|
|
@@ -383,6 +434,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
383
434
|
api.fetch_secret(org: org, secret: secret)
|
|
384
435
|
end
|
|
385
436
|
|
|
437
|
+
def reveal_secret(secret)
|
|
438
|
+
api.reveal_secret(org: org, secret: secret)
|
|
439
|
+
end
|
|
440
|
+
|
|
386
441
|
# identities
|
|
387
442
|
|
|
388
443
|
def fetch_identity(identity, a_gvc = gvc)
|
|
@@ -402,7 +457,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
402
457
|
"--identity", identity_link,
|
|
403
458
|
"--permission", "reveal"
|
|
404
459
|
]
|
|
405
|
-
perform!(
|
|
460
|
+
perform!(cmd)
|
|
406
461
|
end
|
|
407
462
|
|
|
408
463
|
def unbind_identity_from_policy(identity_link, policy, permission: "reveal")
|
|
@@ -412,33 +467,25 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
412
467
|
"--identity", identity_link,
|
|
413
468
|
"--permission", permission
|
|
414
469
|
]
|
|
415
|
-
perform!(
|
|
470
|
+
perform!(cmd)
|
|
416
471
|
end
|
|
417
472
|
|
|
418
473
|
# apply
|
|
419
|
-
def apply_template(data, wait: false) # rubocop:disable Metrics/MethodLength
|
|
474
|
+
def apply_template(data, wait: false) # rubocop:disable Metrics/MethodLength
|
|
420
475
|
Tempfile.create do |f|
|
|
421
476
|
f.write(data)
|
|
422
477
|
f.rewind
|
|
423
|
-
cmd = "cpln apply
|
|
424
|
-
cmd
|
|
425
|
-
|
|
426
|
-
|
|
478
|
+
cmd = ["cpln", "apply", *gvc_org_args, "--file", f.path]
|
|
479
|
+
cmd << "--ready" if wait && ENV.fetch("DISABLE_APPLY_READY", nil).nil?
|
|
480
|
+
spawn_options = {}
|
|
481
|
+
spawn_options[:err] = Shell.tmp_stderr if Shell.should_hide_output?
|
|
427
482
|
|
|
428
|
-
|
|
483
|
+
Shell.debug("CMD", Shellwords.join(cmd))
|
|
429
484
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
else
|
|
433
|
-
Shell.debug("CMD", cmd)
|
|
485
|
+
result = Shell.cmd(*cmd, **spawn_options)
|
|
486
|
+
return parse_apply_result(result[:output]) if result[:success]
|
|
434
487
|
|
|
435
|
-
|
|
436
|
-
if result[:success]
|
|
437
|
-
parse_apply_result(result[:output])
|
|
438
|
-
else
|
|
439
|
-
Shell.abort("Command exited with non-zero status.")
|
|
440
|
-
end
|
|
441
|
-
end
|
|
488
|
+
Shell.abort("Command exited with non-zero status.") unless Shell.tmp_stderr
|
|
442
489
|
end
|
|
443
490
|
end
|
|
444
491
|
|
|
@@ -504,22 +551,15 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
504
551
|
"or ensure that the name is correct."
|
|
505
552
|
end
|
|
506
553
|
|
|
507
|
-
# `output_mode` can be :all, :errors_only or :none.
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
def build_command(cmd, output_mode: nil) # rubocop:disable Metrics/MethodLength
|
|
554
|
+
# `output_mode` can be :all, :errors_only or :none. Redirection is expressed as
|
|
555
|
+
# Process.spawn options so argv commands never need to pass through a shell.
|
|
556
|
+
def command_spawn_options(output_mode = nil)
|
|
511
557
|
output_mode ||= determine_command_output_mode
|
|
558
|
+
return {} if output_mode == :all
|
|
559
|
+
return { out: File::NULL } if output_mode == :errors_only
|
|
560
|
+
return { out: File::NULL, err: File::NULL } if output_mode == :none
|
|
512
561
|
|
|
513
|
-
|
|
514
|
-
when :all
|
|
515
|
-
cmd
|
|
516
|
-
when :errors_only
|
|
517
|
-
"#{cmd} > /dev/null"
|
|
518
|
-
when :none
|
|
519
|
-
"#{cmd} > /dev/null 2>&1"
|
|
520
|
-
else
|
|
521
|
-
raise "Invalid command output mode '#{output_mode}'."
|
|
522
|
-
end
|
|
562
|
+
raise "Invalid command output mode '#{output_mode}'."
|
|
523
563
|
end
|
|
524
564
|
|
|
525
565
|
def determine_command_output_mode
|
|
@@ -533,18 +573,22 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
533
573
|
end
|
|
534
574
|
|
|
535
575
|
def perform(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
|
536
|
-
|
|
576
|
+
validate_argv!(cmd)
|
|
577
|
+
spawn_options = command_spawn_options(output_mode)
|
|
537
578
|
|
|
538
|
-
|
|
579
|
+
debug_cmd = Shellwords.join(cmd)
|
|
580
|
+
Shell.debug("CMD", debug_cmd, sensitive_data_pattern: sensitive_data_pattern)
|
|
539
581
|
|
|
540
|
-
kernel_system_with_pid_handling(cmd)
|
|
582
|
+
kernel_system_with_pid_handling(cmd, **spawn_options)
|
|
541
583
|
end
|
|
542
584
|
|
|
543
585
|
# NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing.
|
|
544
586
|
# Returns true on zero exit, false on non-zero exit, nil when the process was signal-killed.
|
|
545
587
|
# SystemCallError (e.g. cpln binary missing) propagates — startup checks ensure this is unreachable in practice.
|
|
546
|
-
def kernel_system_with_pid_handling(cmd)
|
|
547
|
-
|
|
588
|
+
def kernel_system_with_pid_handling(cmd, **spawn_options)
|
|
589
|
+
validate_argv!(cmd)
|
|
590
|
+
|
|
591
|
+
pid = Process.spawn(*cmd, **spawn_options)
|
|
548
592
|
$child_pids << pid # rubocop:disable Style/GlobalVars
|
|
549
593
|
|
|
550
594
|
_, status = Process.wait2(pid)
|
|
@@ -553,15 +597,65 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
553
597
|
status.exited? ? status.success? : nil
|
|
554
598
|
end
|
|
555
599
|
|
|
600
|
+
def perform_with_output(cmd)
|
|
601
|
+
readers, writers = 2.times.map { IO.pipe }.transpose
|
|
602
|
+
pid = spawn_captured_process(cmd, writers)
|
|
603
|
+
captured_output = drain_captured_output(readers, determine_command_output_mode)
|
|
604
|
+
_, status = Process.wait2(pid)
|
|
605
|
+
reaped = true
|
|
606
|
+
{ output: captured_output, success: status.exited? && status.success? }
|
|
607
|
+
ensure
|
|
608
|
+
[*readers, *writers].compact.each { |pipe| pipe.close unless pipe.closed? }
|
|
609
|
+
$child_pids.delete(pid) if reaped # rubocop:disable Style/GlobalVars
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def spawn_captured_process(cmd, writers)
|
|
613
|
+
validate_argv!(cmd)
|
|
614
|
+
pid = Process.spawn(*cmd, out: writers.fetch(0), err: writers.fetch(1))
|
|
615
|
+
$child_pids << pid # rubocop:disable Style/GlobalVars
|
|
616
|
+
writers.each(&:close)
|
|
617
|
+
pid
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def drain_captured_output(readers, output_mode)
|
|
621
|
+
stream_roles = { readers.fetch(0) => :stdout, readers.fetch(1) => :stderr }
|
|
622
|
+
pending_readers = readers.dup
|
|
623
|
+
captured_output = +""
|
|
624
|
+
|
|
625
|
+
until pending_readers.empty?
|
|
626
|
+
IO.select(pending_readers).first.each do |reader|
|
|
627
|
+
drain_ready_output(reader, stream_roles.fetch(reader), pending_readers, captured_output, output_mode)
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
captured_output
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def drain_ready_output(reader, stream_role, pending_readers, captured_output, output_mode)
|
|
635
|
+
chunk = reader.read_nonblock(4096)
|
|
636
|
+
captured_output << chunk
|
|
637
|
+
return if output_mode == :none || (stream_role == :stdout && output_mode != :all)
|
|
638
|
+
|
|
639
|
+
stream = stream_role == :stdout ? $stdout : $stderr
|
|
640
|
+
stream.write(chunk)
|
|
641
|
+
stream.flush
|
|
642
|
+
rescue IO::WaitReadable
|
|
643
|
+
nil
|
|
644
|
+
rescue EOFError
|
|
645
|
+
pending_readers.delete(reader)
|
|
646
|
+
end
|
|
647
|
+
private :spawn_captured_process, :drain_captured_output, :drain_ready_output
|
|
648
|
+
|
|
556
649
|
def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
|
|
557
650
|
success = perform(cmd, output_mode: output_mode, sensitive_data_pattern: sensitive_data_pattern)
|
|
558
651
|
success || Shell.abort("Command exited with non-zero status.")
|
|
559
652
|
end
|
|
560
653
|
|
|
561
|
-
def perform_yaml(cmd)
|
|
562
|
-
|
|
654
|
+
def perform_yaml(cmd, timeout_seconds: nil, **spawn_options)
|
|
655
|
+
validate_argv!(cmd)
|
|
656
|
+
Shell.debug("CMD", Shellwords.join(cmd))
|
|
563
657
|
|
|
564
|
-
result = Shell.cmd(cmd)
|
|
658
|
+
result = Shell.cmd(*cmd, timeout_seconds: timeout_seconds, **spawn_options)
|
|
565
659
|
YAML.safe_load(result[:output], permitted_classes: [Time]) if result[:success]
|
|
566
660
|
end
|
|
567
661
|
|
|
@@ -569,7 +663,13 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
569
663
|
perform_yaml(cmd) || Shell.abort("Command exited with non-zero status.")
|
|
570
664
|
end
|
|
571
665
|
|
|
572
|
-
def
|
|
573
|
-
"--gvc
|
|
666
|
+
def gvc_org_args
|
|
667
|
+
["--gvc", gvc, "--org", org]
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def validate_argv!(cmd)
|
|
671
|
+
return if cmd.is_a?(Array) && cmd.length >= 2
|
|
672
|
+
|
|
673
|
+
raise ArgumentError, "Commands must be argv arrays with at least two elements."
|
|
574
674
|
end
|
|
575
675
|
end
|
|
@@ -122,6 +122,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
|
|
|
122
122
|
api_json("/org/#{org}/secret/#{secret}", method: :get)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
def reveal_secret(org:, secret:)
|
|
126
|
+
api_json(
|
|
127
|
+
"/org/#{org}/secret/#{secret}/-reveal",
|
|
128
|
+
method: :get,
|
|
129
|
+
request_policy: ControlplaneApiDirect::BEST_EFFORT_SENSITIVE_REQUEST_POLICY
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
125
133
|
def delete_secret(org:, secret:)
|
|
126
134
|
api_json("/org/#{org}/secret/#{secret}", method: :delete)
|
|
127
135
|
end
|