cpl 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: