cpl 1.3.0 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d76f46cb9427fc137ece07123d0ddd0d1add98e1ffa046a95e1394916784676e
4
- data.tar.gz: 266681f356c78d8a4636ae27cb90485a49eda80c27a6199bd3505ddd72cd3451
3
+ metadata.gz: 0e67b960593def42983c0f333119e9edffe55434751769cb1ca25dd8333c1489
4
+ data.tar.gz: 3f201ff14fe51d451c5ab1fd8705e5e544d5e99c87b83e53c54464841a835a1d
5
5
  SHA512:
6
- metadata.gz: 33f9ec2f7af612611d3a55114097f21a2a2cc9605b3bd2813345b9a06f8308a28d204bd71f49f96e2d1a77798e920621651e457801d795b40f3bf51abe91e985
7
- data.tar.gz: d2c82fb6b24cf54e306e83f21b0c8a1b519147e1f8ec66b81442b2ae05f2c752eb18c60cb94c66c2cd11ea921db228116b2bc3ead49c77f1317ce0aa4e290ff1
6
+ metadata.gz: cb075c69a7fa151bdd800186594ef481999075c8377eabf7dcb886c0b30e246fcb5e3ccd6396ff0e8487ac5a8b50c24d01b3dade658cd7c2b4ebfa82cb6546c9
7
+ data.tar.gz: 2a7c0782f57fd543f79fdeb985221dc72bfdba830bbfda81dc966e432007ad314ccd28ba299d0084d446993201f05cabaec40a133c0a2edf7b16af70f3e38348
data/CHANGELOG.md CHANGED
@@ -14,6 +14,23 @@ Changes since the last non-beta release.
14
14
 
15
15
  _Please add entries here for your pull requests that are not yet released._
16
16
 
17
+ ## [1.4.0] - 2024-03-20
18
+
19
+ ### Added
20
+
21
+ - Added new template substitution variables (used by `apply-template` and `setup-app` commands): `{{APP_LOCATION_LINK}}`, `{{APP_IMAGE_LINK}}`, `{{APP_IDENTITY}}`, `{{APP_IDENTITY_LINK}}`, `{{APP_SECRETS}}` and `{{APP_SECRETS_POLICY}}`. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
22
+ - Added `--run-release-phase` option to `deploy-image` command to run release script before deploying (same step as in `promote-app-from-upstream` command). [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
23
+
24
+ ### Changed
25
+
26
+ - Template substitution (used by `apply-template` and `setup-app` commands) now uses double braces (e.g., `APP_ORG` -> `{{APP_ORG}}`). This change is backwards compatible. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
27
+ - Renamed template substitution variable `APP_GVC` to `{{APP_NAME}}` (used by `apply-template` and `setup-app` commands). This change is backwards compatible. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
28
+ - `setup-app` command now automatically binds the app to the secrets policy, as long as both the identity and the policy exist. Added `--skip-secret-access-binding` option to prevent this behavior. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
29
+ - Local API token is now refreshed when it is about to expire. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
30
+ - `apply-template` command now exits with non-zero code if failed to apply any templates. [PR 146](https://github.com/shakacode/heroku-to-control-plane/pull/146) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
31
+
32
+ ## [1.3.0] - 2024-03-19
33
+
17
34
  ### Fixed
18
35
 
19
36
  - Fixed issue where cpln profile was not switched back to `default` if an error happened while running `copy-image-from-upstream` command. [PR 135](https://github.com/shakacode/heroku-to-control-plane/pull/135) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
@@ -139,7 +156,9 @@ _Please add entries here for your pull requests that are not yet released._
139
156
 
140
157
  - Initial release
141
158
 
142
- [Unreleased]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.2.0...HEAD
159
+ [Unreleased]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.4.0...HEAD
160
+ [1.4.0]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.3.0...v1.4.0
161
+ [1.3.0]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.2.0...v1.3.0
143
162
  [1.2.0]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.1.2...v1.2.0
144
163
  [1.1.2]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.1.1...v1.1.2
145
164
  [1.1.1]: https://github.com/shakacode/heroku-to-control-plane/compare/v1.1.0...v1.1.1
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cpl (1.3.0)
4
+ cpl (1.4.0)
5
5
  debug (~> 1.7.1)
6
6
  dotenv (~> 2.8.1)
7
+ jwt (~> 2.8.1)
7
8
  psych (~> 5.1.0)
8
9
  thor (~> 1.2.1)
9
10
 
@@ -13,6 +14,7 @@ GEM
13
14
  addressable (2.8.4)
14
15
  public_suffix (>= 2.0.2, < 6.0)
15
16
  ast (2.4.2)
17
+ base64 (0.2.0)
16
18
  childprocess (4.1.0)
17
19
  crack (0.4.5)
18
20
  rexml
@@ -29,6 +31,8 @@ GEM
29
31
  rdoc
30
32
  reline (>= 0.4.2)
31
33
  json (2.6.3)
34
+ jwt (2.8.1)
35
+ base64
32
36
  overcommit (0.60.0)
33
37
  childprocess (>= 0.6.3, < 5)
34
38
  iniparse (~> 1.4)
data/README.md CHANGED
@@ -190,8 +190,20 @@ aliases:
190
190
 
191
191
  # Allows running the command `cpl setup-app`
192
192
  # instead of `cpl apply-template gvc redis postgres memcached rails sidekiq`.
193
+ #
194
+ # Note:
195
+ # 1. These names correspond to files in the `./controlplane/templates` directory.
196
+ # 2. Each file can contain many objects, such as in the case of templates that create a resource, like `postgres`.
197
+ # 3. While the naming often corresponds to a workload or other object name, the naming is arbitrary.
198
+ # Naming does not need to match anything other than the file name without the `.yml` extension.
193
199
  setup_app_templates:
194
200
  - gvc
201
+
202
+ # These templates are only required if using secrets.
203
+ - identity
204
+ - secrets
205
+ - secrets-policy
206
+
195
207
  - redis
196
208
  - postgres
197
209
  - memcached
@@ -202,11 +214,16 @@ aliases:
202
214
  one_off_workload: rails
203
215
 
204
216
  # Workloads that are for the application itself and are using application Docker images.
217
+ # These are updated with the new image when running the `deploy-image` command,
218
+ # and are also used by the `info`, `ps:`, and `run:cleanup` commands in order to get all of the defined workloads.
219
+ # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
220
+ # and not be listed here.
205
221
  app_workloads:
206
222
  - rails
207
223
  - sidekiq
208
224
 
209
225
  # Additional "service type" workloads, using non-application Docker images.
226
+ # These are only used by the `info`, `ps:` and `run:cleanup` commands in order to get all of the defined workloads.
210
227
  additional_workloads:
211
228
  - redis
212
229
  - postgres
data/cpl.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  spec.add_dependency "debug", "~> 1.7.1"
19
19
  spec.add_dependency "dotenv", "~> 2.8.1"
20
+ spec.add_dependency "jwt", "~> 2.8.1"
20
21
  spec.add_dependency "psych", "~> 5.1.0"
21
22
  spec.add_dependency "thor", "~> 1.2.1"
22
23
 
data/docs/commands.md CHANGED
@@ -21,10 +21,14 @@ This `-a` option is used in most of the commands and will pick all other app con
21
21
  **Preprocessed template variables:**
22
22
 
23
23
  ```
24
- APP_GVC - basically GVC or app name
25
- APP_LOCATION - default location
26
- APP_ORG - organization
27
- APP_IMAGE - will use latest app image
24
+ {{APP_ORG}} - organization name
25
+ {{APP_NAME}} - GVC/app name
26
+ {{APP_LOCATION}} - location, per YML file, ENV, or command line arg
27
+ {{APP_LOCATION_LINK}} - full link for location, ready to be used for the value of `staticPlacement.locationLinks` in the templates
28
+ {{APP_IMAGE}} - latest app image
29
+ {{APP_IMAGE_LINK}} - full link for latest app image, ready to be used for the value of `containers[].image` in the templates
30
+ {{APP_IDENTITY}} - default identity
31
+ {{APP_IDENTITY_LINK}} - full link for identity, ready to be used for the value of `identityLink` in the templates
28
32
  ```
29
33
 
30
34
  ```sh
@@ -111,6 +115,8 @@ cpl delete -a $APP_NAME
111
115
  ### `deploy-image`
112
116
 
113
117
  - Deploys the latest image to app workloads
118
+ - Optionally runs a release script before deploying if specified through `release_script` in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
119
+ - The deploy will fail if the release script exits with a non-zero code or doesn't exist
114
120
 
115
121
  ```sh
116
122
  cpl deploy-image -a $APP_NAME
@@ -251,8 +257,9 @@ cpl open-console -a $APP_NAME
251
257
  - Copies the latest image from upstream, runs a release script (optional), and deploys the image
252
258
  - It performs the following steps:
253
259
  - Runs `cpl copy-image-from-upstream` to copy the latest image from upstream
254
- - Runs a release script if specified through `release_script` in the `.controlplane/controlplane.yml` file
255
260
  - Runs `cpl deploy-image` to deploy the image
261
+ - If `.controlplane/controlplane.yml` includes the `release_script`, `cpl deploy-image` will use the `--run-release-phase` option
262
+ - The deploy will fail if the release script exits with a non-zero code
256
263
 
257
264
  ```sh
258
265
  cpl promote-app-from-upstream -a $APP_NAME -t $UPSTREAM_TOKEN
@@ -399,6 +406,8 @@ cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
399
406
  - Creates an app and all its workloads
400
407
  - Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
401
408
  - This should only be used for temporary apps like review apps, never for persistent apps like production (to update workloads for those, use 'cpl apply-template' instead)
409
+ - Automatically binds the app to the secrets policy, as long as both the identity and the policy exist
410
+ - Use `--skip-secret-access-binding` to prevent the automatic bind
402
411
 
403
412
  ```sh
404
413
  cpl setup-app -a $APP_NAME
data/docs/tips.md CHANGED
@@ -85,7 +85,17 @@ For storing ENVs in the source code, we can use a level of indirection so that y
85
85
  code like `cpln://secret/my-app-review-env-secrets.SECRET_KEY_BASE` and then have the secret value stored at the org
86
86
  level, which applies to your GVCs mapped to that org.
87
87
 
88
- Here is how you do this:
88
+ You can do this during the initial app setup, like this:
89
+
90
+ 1. Add the templates for `identity`, `secrets` and `secrets-policy` to `.controlplane/templates`
91
+ 2. Ensure that the templates are listed in `setup_app_templates` for the app in `.controlplane/controlplane.yml`
92
+ 3. Run `cpl setup-app -a $APP_NAME`
93
+ 4. The identity, secrets and secrets policy will be automatically created, along with the proper binding
94
+ 5. In the upper left "Manage Org" menu, click on "Secrets"
95
+ 6. Find the created secret (it will be in the `$APP_PREFIX-secrets` format) and add the secret env vars there
96
+ 7. Use `cpln://secret/...` in the app to access the secret env vars (e.g., `cpln://secret/$APP_PREFIX-secrets.SOME_VAR`)
97
+
98
+ You can also do it manually after. Here is how you do this:
89
99
 
90
100
  1. In the upper left "Manage Org" menu, click on "Secrets"
91
101
  2. Create a secret with `Secret Type: Dictionary` (e.g., `my-secrets`) and add the secret env vars there
@@ -25,8 +25,20 @@ aliases:
25
25
 
26
26
  # Allows running the command `cpl setup-app`
27
27
  # instead of `cpl apply-template gvc redis postgres memcached rails sidekiq`.
28
- setup:
28
+ #
29
+ # Note:
30
+ # 1. These names correspond to files in the `./controlplane/templates` directory.
31
+ # 2. Each file can contain many objects, such as in the case of templates that create a resource, like `postgres`.
32
+ # 3. While the naming often corresponds to a workload or other object name, the naming is arbitrary.
33
+ # Naming does not need to match anything other than the file name without the `.yml` extension.
34
+ setup_app_templates:
29
35
  - gvc
36
+
37
+ # These templates are only required if using secrets.
38
+ - identity
39
+ - secrets
40
+ - secrets-policy
41
+
30
42
  - redis
31
43
  - postgres
32
44
  - memcached
@@ -37,11 +49,16 @@ aliases:
37
49
  one_off_workload: rails
38
50
 
39
51
  # Workloads that are for the application itself and are using application Docker images.
52
+ # These are updated with the new image when running the `deploy-image` command,
53
+ # and are also used by the `info`, `ps:`, and `run:cleanup` commands in order to get all of the defined workloads.
54
+ # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
55
+ # and not be listed here.
40
56
  app_workloads:
41
57
  - rails
42
58
  - sidekiq
43
59
 
44
60
  # Additional "service type" workloads, using non-application Docker images.
61
+ # These are only used by the `info`, `ps:` and `run:cleanup` commands in order to get all of the defined workloads.
45
62
  additional_workloads:
46
63
  - redis
47
64
  - postgres
@@ -20,10 +20,14 @@ module Command
20
20
  **Preprocessed template variables:**
21
21
 
22
22
  ```
23
- APP_GVC - basically GVC or app name
24
- APP_LOCATION - default location
25
- APP_ORG - organization
26
- APP_IMAGE - will use latest app image
23
+ {{APP_ORG}} - organization name
24
+ {{APP_NAME}} - GVC/app name
25
+ {{APP_LOCATION}} - location, per YML file, ENV, or command line arg
26
+ {{APP_LOCATION_LINK}} - full link for location, ready to be used for the value of `staticPlacement.locationLinks` in the templates
27
+ {{APP_IMAGE}} - latest app image
28
+ {{APP_IMAGE_LINK}} - full link for latest app image, ready to be used for the value of `containers[].image` in the templates
29
+ {{APP_IDENTITY}} - default identity
30
+ {{APP_IDENTITY_LINK}} - full link for identity, ready to be used for the value of `identityLink` in the templates
27
31
  ```
28
32
  DESC
29
33
  EXAMPLES = <<~EX
@@ -36,7 +40,7 @@ module Command
36
40
  ```
37
41
  EX
38
42
 
39
- def call # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
43
+ def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
40
44
  ensure_templates!
41
45
 
42
46
  @created_items = []
@@ -55,6 +59,8 @@ module Command
55
59
 
56
60
  progress.puts if @asked_for_confirmation
57
61
 
62
+ @deprecated_variables = []
63
+
58
64
  pending_templates.each do |template, filename|
59
65
  step("Applying template '#{template}'", abort_on_error: false) do
60
66
  items = apply_template(filename)
@@ -70,9 +76,13 @@ module Command
70
76
  end
71
77
  end
72
78
 
79
+ warn_deprecated_variables
80
+
73
81
  print_created_items
74
82
  print_failed_templates
75
83
  print_skipped_templates
84
+
85
+ exit(1) if @failed_templates.any?
76
86
  end
77
87
 
78
88
  private
@@ -124,17 +134,55 @@ module Command
124
134
  false
125
135
  end
126
136
 
127
- def apply_template(filename)
137
+ def apply_template(filename) # rubocop:disable Metrics/MethodLength
128
138
  data = File.read(filename)
129
- .gsub("APP_GVC", config.app)
130
- .gsub("APP_LOCATION", config.location)
131
- .gsub("APP_ORG", config.org)
132
- .gsub("APP_IMAGE", latest_image)
139
+ .gsub("{{APP_ORG}}", config.org)
140
+ .gsub("{{APP_NAME}}", config.app)
141
+ .gsub("{{APP_LOCATION}}", config.location)
142
+ .gsub("{{APP_LOCATION_LINK}}", app_location_link)
143
+ .gsub("{{APP_IMAGE}}", latest_image)
144
+ .gsub("{{APP_IMAGE_LINK}}", app_image_link)
145
+ .gsub("{{APP_IDENTITY}}", app_identity)
146
+ .gsub("{{APP_IDENTITY_LINK}}", app_identity_link)
147
+ .gsub("{{APP_SECRETS}}", app_secrets)
148
+ .gsub("{{APP_SECRETS_POLICY}}", app_secrets_policy)
149
+
150
+ find_deprecated_variables(data)
151
+
152
+ # Kept for backwards compatibility
153
+ data = data
154
+ .gsub("APP_ORG", config.org)
155
+ .gsub("APP_GVC", config.app)
156
+ .gsub("APP_LOCATION", config.location)
157
+ .gsub("APP_IMAGE", latest_image)
133
158
 
134
159
  # Don't read in YAML.safe_load as that doesn't handle multiple documents
135
160
  cp.apply_template(data)
136
161
  end
137
162
 
163
+ def new_variables
164
+ {
165
+ "APP_ORG" => "{{APP_ORG}}",
166
+ "APP_GVC" => "{{APP_NAME}}",
167
+ "APP_LOCATION" => "{{APP_LOCATION}}",
168
+ "APP_IMAGE" => "{{APP_IMAGE}}"
169
+ }
170
+ end
171
+
172
+ def find_deprecated_variables(data)
173
+ @deprecated_variables.push(*new_variables.keys.select { |old_key| data.include?(old_key) })
174
+ @deprecated_variables = @deprecated_variables.uniq.sort
175
+ end
176
+
177
+ def warn_deprecated_variables
178
+ return unless @deprecated_variables.any?
179
+
180
+ message = "Please replace these variables in the templates, " \
181
+ "as support for them will be removed in a future major version bump:"
182
+ deprecated = @deprecated_variables.map { |old_key| " - #{old_key} -> #{new_variables[old_key]}" }.join("\n")
183
+ progress.puts("\n#{Shell.color("DEPRECATED: #{message}", :yellow)}\n#{deprecated}")
184
+ end
185
+
138
186
  def report_success(item)
139
187
  @created_items.push(item)
140
188
  end
data/lib/command/base.rb CHANGED
@@ -249,6 +249,28 @@ module Command
249
249
  }
250
250
  end
251
251
 
252
+ def self.skip_secret_access_binding_option(required: false)
253
+ {
254
+ name: :skip_secret_access_binding,
255
+ params: {
256
+ desc: "Skips secret access binding",
257
+ type: :boolean,
258
+ required: required
259
+ }
260
+ }
261
+ end
262
+
263
+ def self.run_release_phase_option(required: false)
264
+ {
265
+ name: :run_release_phase,
266
+ params: {
267
+ desc: "Runs release phase",
268
+ type: :boolean,
269
+ required: required
270
+ }
271
+ }
272
+ end
273
+
252
274
  def self.all_options
253
275
  methods.grep(/_option$/).map { |method| send(method.to_s) }
254
276
  end
@@ -372,8 +394,32 @@ module Command
372
394
  @cp ||= Controlplane.new(config)
373
395
  end
374
396
 
375
- def perform(cmd)
376
- system(cmd) || exit(false)
397
+ def perform!(cmd)
398
+ system(cmd) || exit(1)
399
+ end
400
+
401
+ def app_location_link
402
+ "/org/#{config.org}/location/#{config.location}"
403
+ end
404
+
405
+ def app_image_link
406
+ "/org/#{config.org}/image/#{latest_image}"
407
+ end
408
+
409
+ def app_identity
410
+ "#{config.app}-identity"
411
+ end
412
+
413
+ def app_identity_link
414
+ "/org/#{config.org}/gvc/#{config.app}/identity/#{app_identity}"
415
+ end
416
+
417
+ def app_secrets
418
+ "#{config.app_prefix}-secrets"
419
+ end
420
+
421
+ def app_secrets_policy
422
+ "#{app_secrets}-policy"
377
423
  end
378
424
 
379
425
  private
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
4
-
5
3
  module Command
6
4
  class CleanupStaleApps < Base
7
5
  NAME = "cleanup-stale-apps"
@@ -4,14 +4,19 @@ module Command
4
4
  class DeployImage < Base
5
5
  NAME = "deploy-image"
6
6
  OPTIONS = [
7
- app_option(required: true)
7
+ app_option(required: true),
8
+ run_release_phase_option
8
9
  ].freeze
9
- DESCRIPTION = "Deploys the latest image to app workloads"
10
+ DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)"
10
11
  LONG_DESCRIPTION = <<~DESC
11
12
  - Deploys the latest image to app workloads
13
+ - Optionally runs a release script before deploying if specified through `release_script` in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
14
+ - The deploy will fail if the release script exits with a non-zero code or doesn't exist
12
15
  DESC
13
16
 
14
17
  def call # rubocop:disable Metrics/MethodLength
18
+ run_release_script if config.options[:run_release_phase]
19
+
15
20
  deployed_endpoints = {}
16
21
 
17
22
  image = latest_image
@@ -34,5 +39,18 @@ module Command
34
39
  progress.puts(" - #{workload}: #{endpoint}")
35
40
  end
36
41
  end
42
+
43
+ private
44
+
45
+ def run_release_script
46
+ release_script_name = config[:release_script]
47
+ release_script_path = Pathname.new("#{config.app_cpln_dir}/#{release_script_name}").expand_path
48
+
49
+ raise "Can't find release script in '#{release_script_path}'." unless File.exist?(release_script_path)
50
+
51
+ progress.puts("Running release script...\n\n")
52
+ perform!("bash #{release_script_path}")
53
+ progress.puts
54
+ end
37
55
  end
38
56
  end
@@ -12,47 +12,27 @@ module Command
12
12
  - Copies the latest image from upstream, runs a release script (optional), and deploys the image
13
13
  - It performs the following steps:
14
14
  - Runs `cpl copy-image-from-upstream` to copy the latest image from upstream
15
- - Runs a release script if specified through `release_script` in the `.controlplane/controlplane.yml` file
16
15
  - Runs `cpl deploy-image` to deploy the image
16
+ - If `.controlplane/controlplane.yml` includes the `release_script`, `cpl deploy-image` will use the `--run-release-phase` option
17
+ - The deploy will fail if the release script exits with a non-zero code
17
18
  DESC
18
19
 
19
20
  def call
20
- check_release_script
21
21
  copy_image_from_upstream
22
- run_release_script
23
22
  deploy_image
24
23
  end
25
24
 
26
25
  private
27
26
 
28
- def check_release_script
29
- release_script_name = config.current[:release_script]
30
- unless release_script_name
31
- progress.puts("Can't find option 'release_script' for app '#{config.app}' in 'controlplane.yml'. " \
32
- "Skipping release script.\n\n")
33
- return
34
- end
35
-
36
- @release_script_path = Pathname.new("#{config.app_cpln_dir}/#{release_script_name}").expand_path
37
-
38
- raise "Can't find release script in '#{@release_script_path}'." unless File.exist?(@release_script_path)
39
- end
40
-
41
27
  def copy_image_from_upstream
42
28
  Cpl::Cli.start(["copy-image-from-upstream", "-a", config.app, "-t", config.options[:upstream_token]])
43
29
  progress.puts
44
30
  end
45
31
 
46
- def run_release_script
47
- return unless @release_script_path
48
-
49
- progress.puts("Running release script...\n\n")
50
- perform("bash #{@release_script_path}")
51
- progress.puts
52
- end
53
-
54
32
  def deploy_image
55
- Cpl::Cli.start(["deploy-image", "-a", config.app])
33
+ args = []
34
+ args.push("--run-release-phase") if config.current[:release_script]
35
+ Cpl::Cli.start(["deploy-image", "-a", config.app, *args])
56
36
  end
57
37
  end
58
38
  end
data/lib/command/run.rb CHANGED
@@ -104,7 +104,8 @@ module Command
104
104
  container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
105
105
 
106
106
  if config.options["use_local_token"]
107
- container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN", "value" => ControlplaneApiDirect.new.api_token }
107
+ container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN",
108
+ "value" => ControlplaneApiDirect.new.api_token[:token] }
108
109
  end
109
110
 
110
111
  # Create workload clone
@@ -98,7 +98,8 @@ module Command
98
98
  container_spec.delete("ports")
99
99
 
100
100
  container_spec["env"] ||= []
101
- container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN", "value" => ControlplaneApiDirect.new.api_token }
101
+ container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN",
102
+ "value" => ControlplaneApiDirect.new.api_token[:token] }
102
103
  container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
103
104
 
104
105
  # Create workload clone
@@ -4,16 +4,19 @@ module Command
4
4
  class SetupApp < Base
5
5
  NAME = "setup-app"
6
6
  OPTIONS = [
7
- app_option(required: true)
7
+ app_option(required: true),
8
+ skip_secret_access_binding_option
8
9
  ].freeze
9
10
  DESCRIPTION = "Creates an app and all its workloads"
10
11
  LONG_DESCRIPTION = <<~DESC
11
12
  - Creates an app and all its workloads
12
13
  - Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
13
14
  - This should only be used for temporary apps like review apps, never for persistent apps like production (to update workloads for those, use 'cpl apply-template' instead)
15
+ - Automatically binds the app to the secrets policy, as long as both the identity and the policy exist
16
+ - Use `--skip-secret-access-binding` to prevent the automatic bind
14
17
  DESC
15
18
 
16
- def call
19
+ def call # rubocop:disable Metrics/MethodLength
17
20
  templates = config[:setup_app_templates]
18
21
 
19
22
  app = cp.fetch_gvc
@@ -24,6 +27,20 @@ module Command
24
27
  end
25
28
 
26
29
  Cpl::Cli.start(["apply-template", *templates, "-a", config.app])
30
+
31
+ return if config.options[:skip_secret_access_binding]
32
+
33
+ progress.puts
34
+
35
+ if cp.fetch_identity(app_identity).nil? || cp.fetch_policy(app_secrets_policy).nil?
36
+ raise "Can't bind identity to policy: identity '#{app_identity}' or " \
37
+ "policy '#{app_secrets_policy}' doesn't exist. " \
38
+ "Please create them or use `--skip-secret-access-binding` to ignore this message."
39
+ end
40
+
41
+ step("Binding identity to policy") do
42
+ cp.bind_identity_to_policy(app_identity_link, app_secrets_policy)
43
+ end
27
44
  end
28
45
  end
29
46
  end
data/lib/core/config.rb CHANGED
@@ -34,6 +34,10 @@ class Config # rubocop:disable Metrics/ClassLength
34
34
  @app ||= load_app_from_options || load_app_from_env
35
35
  end
36
36
 
37
+ def app_prefix
38
+ current&.fetch(:name)
39
+ end
40
+
37
41
  def location
38
42
  @location ||= load_location_from_options || load_location_from_env || load_location_from_file
39
43
  end
@@ -111,8 +115,12 @@ class Config # rubocop:disable Metrics/ClassLength
111
115
 
112
116
  def find_app_config(app_name1)
113
117
  @app_configs ||= {}
114
- @app_configs[app_name1] ||= apps.find do |app_name2, app_config|
115
- app_matches?(app_name1, app_name2, app_config)
118
+
119
+ @app_configs[app_name1] ||= apps.filter_map do |app_name2, app_config|
120
+ next unless app_matches?(app_name1, app_name2, app_config)
121
+
122
+ app_config[:name] = app_name2
123
+ app_config
116
124
  end&.last
117
125
  end
118
126
 
@@ -300,6 +300,24 @@ class Controlplane # rubocop:disable Metrics/ClassLength
300
300
  api.log_get(org: org, gvc: gvc, workload: workload, from: from, to: to)
301
301
  end
302
302
 
303
+ # identities
304
+
305
+ def fetch_identity(identity, a_gvc = gvc)
306
+ api.fetch_identity(org: org, gvc: a_gvc, identity: identity)
307
+ end
308
+
309
+ # policies
310
+
311
+ def fetch_policy(policy)
312
+ api.fetch_policy(org: org, policy: policy)
313
+ end
314
+
315
+ def bind_identity_to_policy(identity_link, policy)
316
+ cmd = "cpln policy add-binding #{policy} --org #{org} --identity #{identity_link} --permission reveal"
317
+ cmd += " > /dev/null" if Shell.should_hide_output?
318
+ perform!(cmd)
319
+ end
320
+
303
321
  # apply
304
322
  def apply_template(data) # rubocop:disable Metrics/MethodLength
305
323
  Tempfile.create do |f|
@@ -317,7 +335,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
317
335
  Shell.debug("CMD", cmd)
318
336
 
319
337
  result = `#{cmd}`
320
- $CHILD_STATUS.success? ? parse_apply_result(result) : exit(false)
338
+ $CHILD_STATUS.success? ? parse_apply_result(result) : exit(1)
321
339
  end
322
340
  end
323
341
  end
@@ -370,14 +388,14 @@ class Controlplane # rubocop:disable Metrics/ClassLength
370
388
  def perform!(cmd, sensitive_data_pattern: nil)
371
389
  Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
372
390
 
373
- system(cmd) || exit(false)
391
+ system(cmd) || exit(1)
374
392
  end
375
393
 
376
394
  def perform_yaml(cmd)
377
395
  Shell.debug("CMD", cmd)
378
396
 
379
397
  result = `#{cmd}`
380
- $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(false)
398
+ $CHILD_STATUS.success? ? YAML.safe_load(result) : exit(1)
381
399
  end
382
400
 
383
401
  def gvc_org
@@ -106,6 +106,14 @@ class ControlplaneApi # rubocop:disable Metrics/ClassLength
106
106
  api_json("/org/#{org}/domain/#{domain}", method: :patch, body: data)
107
107
  end
108
108
 
109
+ def fetch_identity(org:, gvc:, identity:)
110
+ api_json("/org/#{org}/gvc/#{gvc}/identity/#{identity}", method: :get)
111
+ end
112
+
113
+ def fetch_policy(org:, policy:)
114
+ api_json("/org/#{org}/policy/#{policy}", method: :get)
115
+ end
116
+
109
117
  private
110
118
 
111
119
  def fetch_query_pages(result)
@@ -16,6 +16,7 @@ class ControlplaneApiDirect
16
16
  # ).freeze
17
17
 
18
18
  API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
19
+ API_TOKEN_EXPIRY_SECONDS = 300
19
20
 
20
21
  class << self
21
22
  attr_accessor :trace
@@ -26,7 +27,10 @@ class ControlplaneApiDirect
26
27
  uri = URI("#{api_host(host)}#{url}")
27
28
  request = API_METHODS[method].new(uri)
28
29
  request["Content-Type"] = "application/json"
29
- request["Authorization"] = api_token
30
+
31
+ refresh_api_token if should_refresh_api_token?
32
+
33
+ request["Authorization"] = api_token[:token]
30
34
  request.body = body.to_json if body
31
35
 
32
36
  Shell.debug(method.upcase, "#{uri} #{body&.to_json}")
@@ -62,17 +66,41 @@ class ControlplaneApiDirect
62
66
  end
63
67
 
64
68
  # rubocop:disable Style/ClassVars
65
- def api_token
69
+ def api_token # rubocop:disable Metrics/MethodLength
66
70
  return @@api_token if defined?(@@api_token)
67
71
 
68
- @@api_token = ENV.fetch("CPLN_TOKEN", nil)
69
- @@api_token = `cpln profile token`.chomp if @@api_token.nil?
70
- return @@api_token if @@api_token.match?(API_TOKEN_REGEX)
72
+ @@api_token = {
73
+ token: ENV.fetch("CPLN_TOKEN", nil),
74
+ comes_from_profile: false
75
+ }
76
+ if @@api_token[:token].nil?
77
+ @@api_token = {
78
+ token: `cpln profile token`.chomp,
79
+ comes_from_profile: true
80
+ }
81
+ end
82
+ return @@api_token if @@api_token[:token].match?(API_TOKEN_REGEX)
71
83
 
72
84
  raise "Unknown API token format. " \
73
85
  "Please re-run 'cpln profile login' or set the correct CPLN_TOKEN env variable."
74
86
  end
75
87
 
88
+ # Returns `true` when the token is about to expire in 5 minutes
89
+ def should_refresh_api_token?
90
+ return false unless api_token[:comes_from_profile]
91
+
92
+ payload, = JWT.decode(api_token[:token], nil, false)
93
+ difference_in_seconds = payload["exp"] - Time.now.to_i
94
+
95
+ difference_in_seconds <= API_TOKEN_EXPIRY_SECONDS
96
+ rescue JWT::DecodeError
97
+ false
98
+ end
99
+
100
+ def refresh_api_token
101
+ @@api_token[:token] = `cpln profile token`.chomp
102
+ end
103
+
76
104
  def self.reset_api_token
77
105
  remove_class_variable(:@@api_token) if defined?(@@api_token)
78
106
  end
data/lib/cpl/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end
data/lib/cpl.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require "dotenv/load"
4
5
  require "cgi"
5
6
  require "json"
7
+ require "jwt"
6
8
  require "net/http"
7
9
  require "pathname"
8
10
  require "tempfile"
@@ -19,10 +19,15 @@ aliases:
19
19
  one_off_workload: rails
20
20
 
21
21
  # Workloads that are for the application itself and are using application Docker images.
22
+ # These are updated with the new image when running the `deploy-image` command,
23
+ # and are also used by the `info`, `ps:`, and `run:cleanup` commands in order to get all of the defined workloads.
24
+ # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
25
+ # and not be listed here.
22
26
  app_workloads:
23
27
  - rails
24
28
 
25
29
  # Additional "service type" workloads, using non-application Docker images.
30
+ # These are only used by the `info`, `ps:` and `run:cleanup` commands in order to get all of the defined workloads.
26
31
  additional_workloads:
27
32
  - postgres
28
33
 
@@ -1,15 +1,15 @@
1
1
  # Template setup of the GVC, roughly corresponding to a Heroku app
2
2
  kind: gvc
3
- name: APP_GVC
3
+ name: {{APP_NAME}}
4
4
  spec:
5
5
  # For using templates for test apps, put ENV values here, stored in git repo.
6
6
  # Production apps will have values configured manually after app creation.
7
7
  env:
8
8
  - name: DATABASE_URL
9
- # Password does not matter because host postgres.APP_GVC.cpln.local can only be accessed
9
+ # Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed
10
10
  # locally within CPLN GVC, and postgres running on a CPLN workload is something only for a
11
11
  # test app that lacks persistence.
12
- value: 'postgres://the_user:the_password@postgres.APP_GVC.cpln.local:5432/APP_GVC'
12
+ value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}'
13
13
  - name: RAILS_ENV
14
14
  value: production
15
15
  - name: RAILS_SERVE_STATIC_FILES
@@ -18,4 +18,4 @@ spec:
18
18
  # Part of standard configuration
19
19
  staticPlacement:
20
20
  locationLinks:
21
- - /org/APP_ORG/location/APP_LOCATION
21
+ - {{APP_LOCATION_LINK}}
@@ -106,7 +106,7 @@ bindings:
106
106
  # - use
107
107
  # - view
108
108
  principalLinks:
109
- - //gvc/APP_GVC/identity/postgres-poc-identity
109
+ - //gvc/{{APP_NAME}}/identity/postgres-poc-identity
110
110
  targetKind: secret
111
111
  targetLinks:
112
112
  - //secret/postgres-poc-credentials
@@ -14,7 +14,7 @@ spec:
14
14
  value: debug
15
15
  # Inherit other ENV values from GVC
16
16
  inheritEnv: true
17
- image: '/org/APP_ORG/image/APP_IMAGE'
17
+ image: {{APP_IMAGE_LINK}}
18
18
  # 512 corresponds to a standard 1x dyno type
19
19
  memory: 512Mi
20
20
  ports:
@@ -18,7 +18,7 @@ spec:
18
18
  - rails
19
19
  - db:prepare
20
20
  inheritEnv: true
21
- image: "/org/APP_ORG/image/APP_IMAGE"
21
+ image: {{APP_IMAGE_LINK}}
22
22
  defaultOptions:
23
23
  autoscaling:
24
24
  minScale: 1
@@ -28,4 +28,5 @@ spec:
28
28
  external:
29
29
  outboundAllowCIDR:
30
30
  - 0.0.0.0/0
31
- identityLink: /org/APP_ORG/gvc/APP_GVC/identity/APP_GVC-identity
31
+ # Identity is used for binding workload to secrets
32
+ identityLink: {{APP_IDENTITY_LINK}}
data/templates/gvc.yml CHANGED
@@ -1,13 +1,13 @@
1
1
  kind: gvc
2
- name: APP_GVC
2
+ name: {{APP_NAME}}
3
3
  spec:
4
4
  env:
5
5
  - name: MEMCACHE_SERVERS
6
- value: memcached.APP_GVC.cpln.local
6
+ value: memcached.{{APP_NAME}}.cpln.local
7
7
  - name: REDIS_URL
8
- value: redis://redis.APP_GVC.cpln.local:6379
8
+ value: redis://redis.{{APP_NAME}}.cpln.local:6379
9
9
  - name: DATABASE_URL
10
- value: postgres://postgres:password123@postgres.APP_GVC.cpln.local:5432/APP_GVC
10
+ value: postgres://postgres:password123@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}
11
11
  staticPlacement:
12
12
  locationLinks:
13
- - /org/APP_ORG/location/APP_LOCATION
13
+ - {{APP_LOCATION_LINK}}
@@ -1,2 +1,3 @@
1
+ # Identity is needed to access secrets
1
2
  kind: identity
2
- name: APP_GVC-identity
3
+ name: {{APP_IDENTITY}}
data/templates/rails.yml CHANGED
@@ -7,7 +7,7 @@ spec:
7
7
  cpu: 512m
8
8
  memory: 1Gi
9
9
  inheritEnv: true
10
- image: "/org/APP_ORG/image/APP_IMAGE"
10
+ image: {{APP_IMAGE_LINK}}
11
11
  ports:
12
12
  - number: 3000
13
13
  protocol: http
@@ -23,4 +23,5 @@ spec:
23
23
  - 0.0.0.0/0
24
24
  outboundAllowCIDR:
25
25
  - 0.0.0.0/0
26
- identityLink: /org/APP_ORG/gvc/APP_GVC/identity/APP_GVC-identity
26
+ # Identity is used for binding workload to secrets
27
+ identityLink: {{APP_IDENTITY_LINK}}
@@ -0,0 +1,4 @@
1
+ # Policy is needed to allow identities to access secrets
2
+ kind: policy
3
+ name: {{APP_SECRETS_POLICY}}
4
+ targetKind: secret
@@ -0,0 +1,3 @@
1
+ kind: secret
2
+ name: {{APP_SECRETS}}
3
+ type: dictionary
@@ -13,7 +13,7 @@ spec:
13
13
  - "-C"
14
14
  - config/sidekiq.yml
15
15
  inheritEnv: true
16
- image: "/org/APP_ORG/image/APP_IMAGE"
16
+ image: {{APP_IMAGE_LINK}}
17
17
  ports:
18
18
  - number: 7433
19
19
  protocol: http
@@ -34,4 +34,5 @@ spec:
34
34
  external:
35
35
  outboundAllowCIDR:
36
36
  - 0.0.0.0/0
37
- identityLink: /org/APP_ORG/gvc/APP_GVC/identity/APP_GVC-identity
37
+ # Identity is used for binding workload to secrets
38
+ identityLink: {{APP_IDENTITY_LINK}}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-03-19 00:00:00.000000000 Z
12
+ date: 2024-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: debug
@@ -39,6 +39,20 @@ dependencies:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
41
  version: 2.8.1
42
+ - !ruby/object:Gem::Dependency
43
+ name: jwt
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 2.8.1
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 2.8.1
42
56
  - !ruby/object:Gem::Dependency
43
57
  name: psych
44
58
  requirement: !ruby/object:Gem::Requirement
@@ -298,6 +312,8 @@ files:
298
312
  - templates/postgres.yml
299
313
  - templates/rails.yml
300
314
  - templates/redis.yml
315
+ - templates/secrets-policy.yml
316
+ - templates/secrets.yml
301
317
  - templates/sidekiq.yml
302
318
  homepage: https://github.com/shakacode/heroku-to-control-plane
303
319
  licenses: