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
|
@@ -6,13 +6,14 @@ path — not the public internet.
|
|
|
6
6
|
|
|
7
7
|
This guide covers the recommended setup: **CPLN Cloud Wormhole via an Agent**.
|
|
8
8
|
|
|
9
|
-
> **
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
9
|
+
> **Field casing verified.** Sanitized `cpln identity get <name> -o yaml-slim` exports from live orgs
|
|
10
|
+
> confirmed the `networkResources` nesting and the common `name`, `agentLink`, `FQDN`, and `ports` field casing.
|
|
11
|
+
> The current Control Plane [Identity reference](https://shakadocs.controlplane.com/reference/identity) and
|
|
12
|
+
> [Identity API](https://shakadocs.controlplane.com/api-reference/identity/get-an-identity-by-gvc-and-name)
|
|
13
|
+
> confirm the alternative `IPs` form and optional `resolverIP` spelling; neither appeared in the inspected
|
|
14
|
+
> live resources. Field names are case-sensitive: `cpln apply` can accept an unrecognized field while omitting
|
|
15
|
+
> the intended route, without pointing back to the identity YAML. Before applying in production, check the new
|
|
16
|
+
> block against these casing sources, diff your edited file against a fresh identity export, and consult
|
|
16
17
|
> `cpln <command> --help` for the latest CLI flags.
|
|
17
18
|
|
|
18
19
|
## Why private networking
|
|
@@ -234,11 +235,10 @@ cpln identity get my-app-production-identity \
|
|
|
234
235
|
Edit `identity-db.yaml` and **add** the `networkResources` block — keep every other apply-safe field
|
|
235
236
|
exactly as exported:
|
|
236
237
|
|
|
237
|
-
>
|
|
238
|
-
>
|
|
239
|
-
>
|
|
240
|
-
> `
|
|
241
|
-
> against the original `yaml-slim` export from `cpln identity get` to confirm.
|
|
238
|
+
> **Apply-safe editing:** start from a fresh `yaml-slim` export and diff your edited file against the
|
|
239
|
+
> original before applying. This helps detect unintended changes to exported fields, but it does not validate
|
|
240
|
+
> the newly added `networkResources` block. Check every key in that block, including `agentLink`, `FQDN`/`IPs`,
|
|
241
|
+
> `resolverIP`, and `ports`, against the verified casing sources above.
|
|
242
242
|
|
|
243
243
|
```yaml
|
|
244
244
|
# identity-db.yaml (after edit — abbreviated, your file will have more fields)
|
data/docs/releasing.md
CHANGED
|
@@ -23,6 +23,11 @@ Always update `CHANGELOG.md` before running the release task.
|
|
|
23
23
|
- Fixes, improvements, deprecations, removals, or security updates: patch
|
|
24
24
|
4. Merge the changelog PR before releasing.
|
|
25
25
|
|
|
26
|
+
A minimum Ruby version change is a breaking change. Merge the gemspec floor,
|
|
27
|
+
CI coverage for each supported Ruby minor, and an `Unreleased` breaking-change
|
|
28
|
+
entry together. Keep `Cpflow::VERSION` unchanged in that implementation PR.
|
|
29
|
+
The release task applies the required major version when the change ships.
|
|
30
|
+
|
|
26
31
|
If updating the changelog manually, move the relevant `Unreleased` entries into
|
|
27
32
|
a versioned header:
|
|
28
33
|
|
|
@@ -49,9 +54,13 @@ With no arguments, `rake release`:
|
|
|
49
54
|
|
|
50
55
|
1. Reads the first versioned `CHANGELOG.md` header, such as `## [4.2.0]`.
|
|
51
56
|
2. Uses that version when it is newer than the current gem version.
|
|
52
|
-
3.
|
|
53
|
-
|
|
54
|
-
4.
|
|
57
|
+
3. Keeps using that version when it matches the current gem version, including
|
|
58
|
+
on a retry after a release attempt created the version commit or tag.
|
|
59
|
+
4. Lets the existing-tag policy stop an incomplete retry instead of silently
|
|
60
|
+
selecting a different version.
|
|
61
|
+
5. Falls back to a patch bump only from a stable current version when the
|
|
62
|
+
changelog does not name the current or a newer version. Prereleases require
|
|
63
|
+
an explicit target if the changelog cannot supply one.
|
|
55
64
|
|
|
56
65
|
Other supported forms:
|
|
57
66
|
|
|
@@ -152,4 +161,6 @@ bundle exec rake "sync_github_release[4.2.0]"
|
|
|
152
161
|
```
|
|
153
162
|
|
|
154
163
|
If the tag was pushed but the gem was not published, delete or correct the tag
|
|
155
|
-
and version commit intentionally before trying again.
|
|
164
|
+
and version commit intentionally before trying again. A no-argument retry keeps
|
|
165
|
+
the changelog version authoritative and refuses to reinterpret a prerelease as
|
|
166
|
+
its stable version.
|
|
@@ -11,6 +11,48 @@ set these values directly at the GVC or workload levels, so none of these ENV va
|
|
|
11
11
|
|
|
12
12
|
## Review app secrets
|
|
13
13
|
|
|
14
|
+
For credentials and initialization scripts created by the PostgreSQL template, see
|
|
15
|
+
[PostgreSQL for disposable review apps](./postgres-review-apps.md).
|
|
16
|
+
|
|
17
|
+
For same-repository review apps that need only disposable credentials, opt in on
|
|
18
|
+
the dynamically matched review-app entry in `.controlplane/controlplane.yml`:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
apps:
|
|
22
|
+
my-app-review:
|
|
23
|
+
match_if_app_name_starts_with: true
|
|
24
|
+
generated_review_secret_keys:
|
|
25
|
+
- SECRET_KEY_BASE
|
|
26
|
+
- RENDERER_PASSWORD
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`cpflow setup-app` then creates a separate `APP_NAME-secrets` dictionary and
|
|
30
|
+
policy for each PR app, tags both resources with the exact app name, fills each
|
|
31
|
+
configured key with a random 256-bit hex value, and never prints the values.
|
|
32
|
+
`--refresh-templates` fills missing keys without rotating existing values.
|
|
33
|
+
Initial template application skips templates named for the generated dictionary
|
|
34
|
+
and policy, so templates cannot replace its values or broaden its access.
|
|
35
|
+
Refresh also skips the generated policy template.
|
|
36
|
+
Setup refuses to reuse a dictionary without the exact app tag or a policy that
|
|
37
|
+
targets another secret or binds another principal; it rechecks the policy
|
|
38
|
+
immediately before granting the app identity. Keep this
|
|
39
|
+
setting off persistent apps and do
|
|
40
|
+
not combine it with custom `secrets_name` or `secrets_policy_name`. Templates
|
|
41
|
+
that use `{{APP_SECRETS}}` automatically point at the PR-specific dictionary.
|
|
42
|
+
`cpflow delete` and delete-mode stale cleanup remove that dictionary and policy
|
|
43
|
+
before removing the app, provided the policy still targets the dictionary and
|
|
44
|
+
has no other bindings. Generated dictionaries carry an app marker; if a prior
|
|
45
|
+
attempt removed the policy first, cleanup can use that marker to remove only the
|
|
46
|
+
matching disposable dictionary. An unmarked policy is also left for inspection
|
|
47
|
+
if its dictionary is already gone. If the GVC was removed separately before disposable credential cleanup finished, rerun
|
|
48
|
+
`cpflow delete` with the same app name to finish secret cleanup. The generated
|
|
49
|
+
delete action calls that same recovery path when the GVC is already absent.
|
|
50
|
+
Cleanup still checks the app-specific names and tag if the key-generation option
|
|
51
|
+
was later removed. Unexpected grants or unmarked secrets leave those resources
|
|
52
|
+
for inspection.
|
|
53
|
+
This generates credentials only; external licenses, database accounts, and
|
|
54
|
+
provider tokens still require their own review-only provisioning.
|
|
55
|
+
|
|
14
56
|
Review apps run application code from pull requests. For repositories with external contributors, do not put sensitive
|
|
15
57
|
production or long-lived staging secrets in review apps. A secret reference such as `cpln://secret/...` protects the
|
|
16
58
|
value while it is stored in Control Plane configuration, but the value becomes readable by application code after it is
|
data/docs/sidebars.ts
CHANGED
data/examples/controlplane.yml
CHANGED
|
@@ -104,6 +104,11 @@ aliases:
|
|
|
104
104
|
# If not specified, defaults to 21600 (6 hours).
|
|
105
105
|
runner_job_timeout: 1000
|
|
106
106
|
|
|
107
|
+
# Sets how long `cpflow run` waits for Control Plane to reconcile a cron job status
|
|
108
|
+
# after the non-interactive command prints its completion marker.
|
|
109
|
+
# If not specified, defaults to 1200 (20 minutes).
|
|
110
|
+
runner_job_status_reconciliation_timeout: 1200
|
|
111
|
+
|
|
107
112
|
# Apps with a deployed image created before this amount of days will be listed for deletion
|
|
108
113
|
# when running the command `cpflow cleanup-stale-apps`.
|
|
109
114
|
stale_app_image_deployed_days: 5
|
|
@@ -32,9 +32,9 @@ module Command
|
|
|
32
32
|
<<~PROMPT
|
|
33
33
|
Set up Control Plane GitHub Flow for this repo. First make sure the `cpflow` CLI is available: use the repo's existing `bundle exec cpflow` if present, otherwise install the published `cpflow` Ruby gem with `gem install cpflow`; if neither is possible, stop and report that blocker. Use the same `cpflow` invocation for the rest of the rollout. Start with `cpflow github-flow-readiness` and stop on any reported blockers. The repo must be deployable from a clean clone: published package versions, complete runtime scaffold, and a production Dockerfile that can build the app. If any package version is unpublished, inaccessible from CI, or requires credentials that are not already modeled in the repo or GitHub settings, stop and report the blocker instead of generating workflow files. If the repo is a legacy sample pinned to an obsolete Ruby or Bundler toolchain, if it does not even have a production Dockerfile yet, or if it is a monorepo without an already-decided single app boundary for this flow, stop and report that as a prerequisite instead of forcing the rollout.
|
|
34
34
|
|
|
35
|
-
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers
|
|
35
|
+
If `.controlplane/` is missing, run `cpflow generate`. Treat the generated app names as the repo-name default (`#{inferred_app_prefix}`) and rename them only if the project needs a different prefix. Then run `cpflow generate-github-actions` (or `cpflow generate-github-actions --staging-branch BRANCH` when staging should deploy from a branch other than `main`/`master`), keep review apps opt-in via `+review-app-deploy`, make sure any `STAGING_APP_BRANCH` repository variable is also present in the generated staging workflow's `on.push.branches` filter, and list the GitHub secrets and variables that must be configured. Do not hand-edit duplicated upstream refs into the generated wrappers. Keep every generated Control Plane Flow source pin synchronized at the same `vX.Y.Z`: each reusable-workflow `uses: ...@vX.Y.Z` ref, the production promotion `.cpflow` checkout ref, and its `control_plane_flow_ref` setup input. By default, `cpflow generate-github-actions` derives these values from the installed `cpflow` gem version; temporary explicit ref overrides are for unreleased testing and must keep all three values synchronized. Keep the generated `.github/actions/cpflow-*` files from that same gem unchanged; upstream reusable workflows invoke those checked-in local actions and separately check out the matching runtime source at `.cpflow`. When bumping the `cpflow` gem in a downstream repo, `cpflow update-github-actions` refreshes local actions and helpers while preserving all top-level workflows, including their refs and triggers. Use `cpflow update-github-actions --workflows FILE...` to explicitly select wrappers for replacement, then review the diff and reapply downstream customizations. A differing existing validator blocks all writes: preserve its downstream checks in executable `bin/test-cpflow-github-flow-custom`, then remove or rename the old generated validator before retrying. The generated validator invokes that preserved extension and propagates its failures. Follow `docs/ci-automation.md` for the migration procedure, keep source pins compatible with the updated actions, and run `bin/test-cpflow-github-flow` in the same PR (or `bin/test-cpflow-github-flow bundle exec cpflow` when using Bundler). Keep the normal generated review-app setup simple: review apps require only `CPLN_TOKEN_STAGING` when the generated review app config can be inferred. For public demos, starter staging apps, and long-lived review apps, keep the app workload `type: standard` with one warm replica, set its autoscaling metric to `disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and memory allocation at that fixed replica count. Shared Postgres and other stateful workloads are the usual exceptions and should stay manually sized; Capacity AI is for supported stateless app/service workloads. If true idle scale-to-zero is explicitly required, create a separate `serverless` workload before first deploy or plan a delete/recreate migration because Control Plane will not change an existing `standard` workload to `serverless` in place. For shared review-app resources such as one staging database, use `shared_secret_grants` and `{{SHARED_SECRET_DATABASE}}` placeholders instead of hardcoding the base app secret name; this keeps review-app policy binding and cleanup automatic while avoiding per-PR database cost. Document the one-time Control Plane bootstrap command for persistent staging and production apps with `cpflow setup-app --skip-post-creation-hook`; for existing apps or later template updates, document `cpflow apply-template` and the need for the app identity to have `reveal` on the app secret policy. Do not imply the staging deploy or promotion workflows create those persistent GVCs. For production promotion, document a protected `production` GitHub Environment with required reviewers, prevent self-review, and `CPLN_TOKEN_PRODUCTION` stored as an environment secret, not as a repository or organization secret.
|
|
36
36
|
|
|
37
|
-
Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold
|
|
37
|
+
Keep Node available in the final image if asset compilation or SSR depends on ExecJS, Yarn, `pnpm`, or npm after the main install layer. Make sure the generated Dockerfile uses a Ruby base image compatible with the app's declared Ruby requirement. Preserve repo-defined frontend build hooks: if `config/shakapacker.yml` defines a `precompile_hook`, or React on Rails enables `config.auto_load_bundle = true`, confirm the generated Dockerfile runs that codegen step before `rails assets:precompile`. If `config/database.yml` shows SQLite in production, confirm that the generated scaffold redirects every production database file into persistent `/app/data` (or keeps it under persistent `/app/storage`) without mounting over the image's `/app/db` migrations, stores Active Storage under `/app/storage`, and runs `rails db:prepare`; otherwise keep the default Postgres workload. If the public workload is not named `rails`, set `PRIMARY_WORKLOAD` or adjust the generated workflows. Inspect the Dockerfile and package sources for private GitHub dependencies or `RUN --mount=type=ssh`; if present, wire `DOCKER_BUILD_SSH_KEY`, optionally set `DOCKER_BUILD_SSH_KNOWN_HOSTS` for non-GitHub SSH hosts, and keep `DOCKER_BUILD_EXTRA_ARGS` to newline-delimited single tokens such as `--build-arg=FOO=bar`.
|
|
38
38
|
|
|
39
39
|
Run the real local validations you can: Docker build if feasible, repo tests or smoke checks, YAML validation, and any CI-equivalent build steps. Push the branch and check the GitHub Actions results. Only stop early for a real external blocker or a product decision that changes scope.
|
|
40
40
|
PROMPT
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../core/disposable_postgres_helpers"
|
|
4
|
+
|
|
3
5
|
module Command
|
|
4
6
|
class ApplyTemplate < Base # rubocop:disable Metrics/ClassLength
|
|
5
7
|
NAME = "apply-template"
|
|
@@ -10,7 +12,9 @@ module Command
|
|
|
10
12
|
location_option,
|
|
11
13
|
skip_confirm_option,
|
|
12
14
|
add_app_identity_option,
|
|
13
|
-
preserve_existing_runtime_option
|
|
15
|
+
preserve_existing_runtime_option,
|
|
16
|
+
exclude_secret_template_option,
|
|
17
|
+
exclude_policy_template_option
|
|
14
18
|
].freeze
|
|
15
19
|
DESCRIPTION = "Applies application-specific configs from templates"
|
|
16
20
|
LONG_DESCRIPTION = <<~DESC
|
|
@@ -19,6 +23,7 @@ module Command
|
|
|
19
23
|
- Picks templates from the `.controlplane/templates` directory
|
|
20
24
|
- Templates are ordinary Control Plane templates but with variable preprocessing
|
|
21
25
|
- Use `--preserve-existing-runtime` to retain each workload container's configured app image, even when the workload is unready, and skip existing secret resources entirely while applying other template changes
|
|
26
|
+
- Use `--exclude-secret-template NAME` and `--exclude-policy-template NAME` to exclude exact named templates without changing workload image handling
|
|
22
27
|
- Missing or invalid workload images use only an unambiguous app image from ready workloads; refresh fails before applying templates when no safe fallback exists
|
|
23
28
|
|
|
24
29
|
**Preprocessed template variables:**
|
|
@@ -61,7 +66,8 @@ module Command
|
|
|
61
66
|
@skipped_templates = []
|
|
62
67
|
|
|
63
68
|
templates = @template_parser.parse(@names_to_filenames.values)
|
|
64
|
-
templates =
|
|
69
|
+
templates = filter_existing_resources(templates)
|
|
70
|
+
DisposablePostgresHelpers.new(config, cp).prepare_templates!(templates)
|
|
65
71
|
pending_templates = confirm_templates(templates)
|
|
66
72
|
add_app_identity_template(pending_templates) if config.options[:add_app_identity]
|
|
67
73
|
pending_templates.each do |template|
|
|
@@ -77,6 +83,27 @@ module Command
|
|
|
77
83
|
|
|
78
84
|
private
|
|
79
85
|
|
|
86
|
+
def filter_existing_resources(templates)
|
|
87
|
+
templates = preserve_existing_runtime(templates) if config.options[:preserve_existing_runtime]
|
|
88
|
+
templates = exclude_named_template(templates, :exclude_secret_template, "secret")
|
|
89
|
+
exclude_named_template(templates, :exclude_policy_template, "policy")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def exclude_named_template(templates, option, kind)
|
|
93
|
+
name = config.options[option]
|
|
94
|
+
return templates unless name
|
|
95
|
+
raise "--#{option.to_s.tr('_', '-')} requires a #{kind} name." if name.empty?
|
|
96
|
+
|
|
97
|
+
templates.filter_map do |template|
|
|
98
|
+
if template["kind"] == kind && template["name"] == name
|
|
99
|
+
report_skipped(template)
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
template
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
80
107
|
def template_kind(template)
|
|
81
108
|
case template["kind"]
|
|
82
109
|
when "gvc"
|
|
@@ -154,13 +181,18 @@ module Command
|
|
|
154
181
|
cache_existing_workloads(templates)
|
|
155
182
|
ready_fallback_image = unambiguous_ready_app_image
|
|
156
183
|
|
|
184
|
+
skip_existing_secret_resources(templates).each do |template|
|
|
185
|
+
preserve_workload_images(template, ready_fallback_image) if template["kind"] == "workload"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def skip_existing_secret_resources(templates)
|
|
157
190
|
templates.filter_map do |template|
|
|
158
191
|
if template["kind"] == "secret" && cp.fetch_secret(template["name"])
|
|
159
192
|
report_skipped(template)
|
|
160
193
|
next
|
|
161
194
|
end
|
|
162
195
|
|
|
163
|
-
preserve_workload_images(template, ready_fallback_image) if template["kind"] == "workload"
|
|
164
196
|
template
|
|
165
197
|
end
|
|
166
198
|
end
|
|
@@ -312,7 +344,7 @@ module Command
|
|
|
312
344
|
return unless @skipped_templates.any?
|
|
313
345
|
|
|
314
346
|
skipped = @skipped_templates.map { |template| " - [#{template_kind(template)}] #{template['name']}" }.join("\n")
|
|
315
|
-
progress.puts("\n#{Shell.color('Skipped templates
|
|
347
|
+
progress.puts("\n#{Shell.color('Skipped templates:', :blue)}\n#{skipped}")
|
|
316
348
|
end
|
|
317
349
|
end
|
|
318
350
|
end
|
data/lib/command/base.rb
CHANGED
|
@@ -483,6 +483,28 @@ module Command
|
|
|
483
483
|
}
|
|
484
484
|
end
|
|
485
485
|
|
|
486
|
+
def self.exclude_secret_template_option(required: false)
|
|
487
|
+
{
|
|
488
|
+
name: :exclude_secret_template,
|
|
489
|
+
params: {
|
|
490
|
+
desc: "Excludes one named secret template while applying templates",
|
|
491
|
+
type: :string,
|
|
492
|
+
required: required
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def self.exclude_policy_template_option(required: false)
|
|
498
|
+
{
|
|
499
|
+
name: :exclude_policy_template,
|
|
500
|
+
params: {
|
|
501
|
+
desc: "Excludes one named policy template while applying templates",
|
|
502
|
+
type: :string,
|
|
503
|
+
required: required
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
end
|
|
507
|
+
|
|
486
508
|
def self.skip_pre_deletion_hook_option(required: false)
|
|
487
509
|
{
|
|
488
510
|
name: :skip_pre_deletion_hook,
|
|
@@ -688,15 +710,46 @@ module Command
|
|
|
688
710
|
end
|
|
689
711
|
|
|
690
712
|
def shared_secret_policy_targets_secret?(grant, policy)
|
|
713
|
+
policy_targets_secret?(policy, grant.fetch(:secret_name))
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
def policy_targets_secret?(policy, secret_name)
|
|
691
717
|
target_links = Array(policy["targetLinks"])
|
|
692
718
|
|
|
693
719
|
policy["targetKind"] == "secret" &&
|
|
694
720
|
target_links.one? &&
|
|
695
|
-
|
|
721
|
+
secret_policy_target_links(secret_name).include?(target_links.first)
|
|
696
722
|
end
|
|
697
723
|
|
|
698
|
-
def
|
|
699
|
-
|
|
724
|
+
def generated_review_app_tag(resource)
|
|
725
|
+
tags = resource["tags"]
|
|
726
|
+
return nil unless tags.is_a?(Hash)
|
|
727
|
+
|
|
728
|
+
tags[::Config::GENERATED_REVIEW_APP_TAG]
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def generated_review_secret_owned_by_app?(secret, secret_name = config.secrets)
|
|
732
|
+
secret.is_a?(Hash) && secret["name"] == secret_name && secret["type"] == "dictionary" &&
|
|
733
|
+
generated_review_app_tag(secret) == config.app
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def generated_review_policy_binding_state(policy)
|
|
737
|
+
bindings = Array(policy["bindings"])
|
|
738
|
+
return :unbound if bindings.empty?
|
|
739
|
+
return :unsafe unless generated_review_policy_bindings_for_app?(bindings)
|
|
740
|
+
|
|
741
|
+
case bindings.map { |binding| binding["permissions"] }.uniq
|
|
742
|
+
when [[]] then :unbound
|
|
743
|
+
when [["reveal"]] then :bound
|
|
744
|
+
else :unsafe
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def generated_review_policy_bindings_for_app?(bindings)
|
|
749
|
+
bindings.all? { |binding| Array(binding["principalLinks"]) == [config.identity_link] }
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def secret_policy_target_links(secret_name)
|
|
700
753
|
[
|
|
701
754
|
"//secret/#{secret_name}",
|
|
702
755
|
"/org/#{config.org}/secret/#{secret_name}"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Command
|
|
4
|
-
class CleanupStaleApps < Base
|
|
4
|
+
class CleanupStaleApps < Base # rubocop:disable Metrics/ClassLength
|
|
5
5
|
CLEANUP_MODE_OPTION = {
|
|
6
6
|
name: :mode,
|
|
7
7
|
params: {
|
|
@@ -24,7 +24,7 @@ module Command
|
|
|
24
24
|
LONG_DESCRIPTION = <<~DESC
|
|
25
25
|
- Acts on stale apps based on the creation date of the latest image, or the GVC if no images exist
|
|
26
26
|
- With `--mode=delete` (default): deletes the whole app (GVC with all workloads, all volumesets and all images), and unbinds the app from the secrets policy and any configured `shared_secret_grants` policies as long as both the identity and each policy exist (and are bound)
|
|
27
|
-
- With `--mode=stop`: suspends
|
|
27
|
+
- With `--mode=stop`: suspends configured workloads that exist in the live GVC through Control Plane — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
|
|
28
28
|
- `--mode=stop` only suspends workloads listed in `app_workloads` + `additional_workloads`; workloads present in the live GVC but missing from the config are skipped silently
|
|
29
29
|
- `--mode=stop` returns once each workload is marked suspended; it does not wait for the workload to reach a not-ready state
|
|
30
30
|
- Specify the amount of days after an app should be considered stale through `stale_app_image_deployed_days` in the `.controlplane/controlplane.yml` file
|
|
@@ -101,14 +101,38 @@ module Command
|
|
|
101
101
|
def process_app(app)
|
|
102
102
|
if mode == "stop"
|
|
103
103
|
progress.puts("Stopping app '#{app}'")
|
|
104
|
-
|
|
104
|
+
stop_configured_live_workloads(app)
|
|
105
105
|
else
|
|
106
106
|
run_cpflow_command("delete", "-a", app, "--yes")
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
+
def stop_configured_live_workloads(app)
|
|
111
|
+
app_config = config.find_app_config(app)
|
|
112
|
+
raise "Can't find config for stale app '#{app}'." unless app_config
|
|
113
|
+
|
|
114
|
+
configured_workloads = required_app_option(app_config, app, :app_workloads) +
|
|
115
|
+
required_app_option(app_config, app, :additional_workloads)
|
|
116
|
+
live_workloads = (cp.fetch_workloads(app)&.fetch("items", []) || []).map { |workload| workload.fetch("name") }
|
|
117
|
+
|
|
118
|
+
(configured_workloads & live_workloads).each do |workload|
|
|
119
|
+
step("Stopping workload '#{workload}'") do
|
|
120
|
+
cp.set_workload_suspend(workload, true, app, missing_ok: true)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def required_app_option(app_config, app, option)
|
|
126
|
+
raise "Can't find option '#{option}' for app '#{app}' in 'controlplane.yml'." unless app_config.key?(option)
|
|
127
|
+
|
|
128
|
+
value = app_config.fetch(option)
|
|
129
|
+
raise "Option '#{option}' for app '#{app}' in 'controlplane.yml' must be an array." unless value.is_a?(Array)
|
|
130
|
+
|
|
131
|
+
value
|
|
132
|
+
end
|
|
133
|
+
|
|
110
134
|
def action_description
|
|
111
|
-
mode == "stop" ? "suspend
|
|
135
|
+
mode == "stop" ? "suspend configured workloads in" : "delete"
|
|
112
136
|
end
|
|
113
137
|
|
|
114
138
|
def mode
|
|
@@ -80,6 +80,9 @@ module Command
|
|
|
80
80
|
upstream_image = cp.latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
|
|
81
81
|
@commit = cp.extract_image_commit(upstream_image)
|
|
82
82
|
@upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
|
|
83
|
+
rescue ControlplaneApiDirect::ForbiddenError => e
|
|
84
|
+
Shell.write_to_tmp_stderr(e.message)
|
|
85
|
+
false
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|