cpl 2.0.1 → 2.1.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/.gitignore +2 -1
- data/CHANGELOG.md +111 -85
- data/CONTRIBUTING.md +2 -2
- data/Gemfile.lock +2 -2
- data/README.md +14 -5
- data/cpl.gemspec +1 -1
- data/docs/commands.md +30 -6
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/tips.md +1 -40
- data/examples/controlplane.yml +12 -3
- data/lib/command/apply_template.rb +70 -80
- data/lib/command/base.rb +82 -71
- data/lib/command/build_image.rb +2 -2
- data/lib/command/cleanup_images.rb +1 -1
- data/lib/command/cleanup_stale_apps.rb +1 -1
- data/lib/command/copy_image_from_upstream.rb +3 -3
- data/lib/command/delete.rb +17 -5
- data/lib/command/deploy_image.rb +6 -21
- data/lib/command/doctor.rb +47 -0
- data/lib/command/latest_image.rb +1 -1
- data/lib/command/no_command.rb +1 -0
- data/lib/command/promote_app_from_upstream.rb +1 -1
- data/lib/command/run.rb +17 -11
- data/lib/command/setup_app.rb +80 -16
- data/lib/command/test.rb +1 -0
- data/lib/command/version.rb +1 -0
- data/lib/core/config.rb +40 -12
- data/lib/core/controlplane.rb +53 -0
- data/lib/core/controlplane_api.rb +13 -7
- data/lib/core/controlplane_api_direct.rb +1 -1
- data/lib/core/doctor_service.rb +104 -0
- data/lib/core/helpers.rb +10 -0
- data/lib/core/shell.rb +7 -0
- data/lib/core/template_parser.rb +76 -0
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +25 -11
- data/templates/app.yml +0 -5
- metadata +8 -7
- data/googlee2da545df05d92f9.html +0 -1
- data/lib/core/controlplane_api_cli.rb +0 -10
- data/templates/secrets.yml +0 -11
data/docs/tips.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
1. [GVCs vs. Orgs](#gvcs-vs-orgs)
|
4
4
|
2. [RAM](#ram)
|
5
5
|
3. [Remote IP](#remote-ip)
|
6
|
-
4. [ENV Values](
|
6
|
+
4. [Secrets and ENV Values](/docs/secrets-and-env-values.md)
|
7
7
|
5. [CI](#ci)
|
8
8
|
6. [Memcached](#memcached)
|
9
9
|
7. [Sidekiq](#sidekiq)
|
@@ -70,45 +70,6 @@ pick those up and automatically populate `request.remote_ip`.
|
|
70
70
|
|
71
71
|
So `REMOTE_ADDR` should not be used directly, only `request.remote_ip`.
|
72
72
|
|
73
|
-
## ENV Values
|
74
|
-
|
75
|
-
You can store ENV values used by a container (within a workload) within Control Plane at the following levels:
|
76
|
-
|
77
|
-
1. Workload Container
|
78
|
-
2. GVC
|
79
|
-
|
80
|
-
For your "review apps," it is convenient to have simple ENVs stored in plain text in your source code. You will want to
|
81
|
-
keep some ENVs, like the Rails' `SECRET_KEY_BASE`, out of your source code. For staging and production apps, you will
|
82
|
-
set these values directly at the GVC or workload levels, so none of these ENV values are committed to the source code.
|
83
|
-
|
84
|
-
For storing ENVs in the source code, we can use a level of indirection so that you can store an ENV value in your source
|
85
|
-
code like `cpln://secret/my-app-review-env-secrets.SECRET_KEY_BASE` and then have the secret value stored at the org
|
86
|
-
level, which applies to your GVCs mapped to that org.
|
87
|
-
|
88
|
-
You can do this during the initial app setup, like this:
|
89
|
-
|
90
|
-
1. Add the templates for `app` and `secrets` to `.controlplane/templates`
|
91
|
-
2. Ensure that the `app` template includes the `identity`
|
92
|
-
3. Ensure that the `app` template is listed in `setup_app_templates` for the app in `.controlplane/controlplane.yml`
|
93
|
-
4. Run `cpl apply-template secrets -a $APP_NAME` (one-time setup)
|
94
|
-
5. Run `cpl setup-app -a $APP_NAME`
|
95
|
-
6. The secrets, secrets policy and identity will be automatically created, along with the proper binding
|
96
|
-
7. In the Control Plane console, upper left "Manage Org" menu, click on "Secrets"
|
97
|
-
8. Find the created secret (it will be in the `$APP_PREFIX-secrets` format) and add the secret env vars there
|
98
|
-
9. Use `cpln://secret/...` in the app to access the secret env vars (e.g., `cpln://secret/$APP_PREFIX-secrets.SOME_VAR`)
|
99
|
-
|
100
|
-
Here are the manual steps for reference. We recommend that you follow the steps above:
|
101
|
-
|
102
|
-
1. In the upper left of the Control Plane console, "Manage Org" menu, click on "Secrets"
|
103
|
-
2. Create a secret with `Secret Type: Dictionary` (e.g., `my-secrets`) and add the secret env vars there
|
104
|
-
3. In the upper left "Manage GVC" menu, click on "Identities"
|
105
|
-
4. Create an identity (e.g., `my-identity`)
|
106
|
-
5. Navigate to the workload that you want to associate with the identity created
|
107
|
-
6. Click "Identity" on the left menu and select the identity created
|
108
|
-
7. In the lower left "Access Control" menu, click on "Policies"
|
109
|
-
8. Create a policy with `Target Kind: Secret` and add a binding with the `reveal` permission for the identity created
|
110
|
-
9. Use `cpln://secret/...` in the app to access the secret env vars (e.g., `cpln://secret/my-secrets.SOME_VAR`)
|
111
|
-
|
112
73
|
## CI
|
113
74
|
|
114
75
|
**Note:** Docker builds much slower on Apple Silicon, so try configuring CI to build the images when using Apple
|
data/examples/controlplane.yml
CHANGED
@@ -31,9 +31,6 @@ aliases:
|
|
31
31
|
# 2. Each file can contain many objects, such as in the case of templates that create a resource, like `postgres`.
|
32
32
|
# 3. While the naming often corresponds to a workload or other object name, the naming is arbitrary.
|
33
33
|
# Naming does not need to match anything other than the file name without the `.yml` extension.
|
34
|
-
#
|
35
|
-
# If you're going to use secrets, you need to apply the `secrets.yml` template separately (one-time setup):
|
36
|
-
# `cpl apply-template secrets -a my-app`
|
37
34
|
setup_app_templates:
|
38
35
|
- app
|
39
36
|
- redis
|
@@ -42,6 +39,9 @@ aliases:
|
|
42
39
|
- rails
|
43
40
|
- sidekiq
|
44
41
|
|
42
|
+
# Uncomment next line to skips secrets setup when running `cpl setup-app`.
|
43
|
+
# skip_secrets_setup: true
|
44
|
+
|
45
45
|
# Only needed if using a custom secrets name.
|
46
46
|
# The default is '{APP_PREFIX}-secrets'. For example:
|
47
47
|
# - for an app 'my-app-staging' with `match_if_app_name_starts_with` set to `false`,
|
@@ -108,6 +108,15 @@ apps:
|
|
108
108
|
# e.g., "my-app-review-pr123", "my-app-review-anything-goes", etc.
|
109
109
|
match_if_app_name_starts_with: true
|
110
110
|
|
111
|
+
# Hooks can be either a script path that exists in the app image or a command.
|
112
|
+
# They're run in the context of `cpl run` with the latest image.
|
113
|
+
hooks:
|
114
|
+
# Used by the command `cpl setup-app` to run a hook after creating the app.
|
115
|
+
post_creation: bundle exec rake db:prepare
|
116
|
+
|
117
|
+
# Used by the command `cpl delete` to run a hook before deleting the app.
|
118
|
+
pre_deletion: bundle exec rake db:drop
|
119
|
+
|
111
120
|
my-app-production:
|
112
121
|
<<: *common
|
113
122
|
|
@@ -8,7 +8,8 @@ module Command
|
|
8
8
|
OPTIONS = [
|
9
9
|
app_option(required: true),
|
10
10
|
location_option,
|
11
|
-
skip_confirm_option
|
11
|
+
skip_confirm_option,
|
12
|
+
add_app_identity_option
|
12
13
|
].freeze
|
13
14
|
DESCRIPTION = "Applies application-specific configs from templates"
|
14
15
|
LONG_DESCRIPTION = <<~DESC
|
@@ -39,45 +40,27 @@ module Command
|
|
39
40
|
cpl apply-template app postgres redis rails -a $APP_NAME
|
40
41
|
```
|
41
42
|
EX
|
43
|
+
VALIDATIONS = %w[config templates].freeze
|
44
|
+
|
45
|
+
def call # rubocop:disable Metrics/MethodLength
|
46
|
+
@template_parser = TemplateParser.new(config)
|
47
|
+
@names_to_filenames = config.args.to_h do |name|
|
48
|
+
[name, @template_parser.template_filename(name)]
|
49
|
+
end
|
42
50
|
|
43
|
-
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
44
51
|
ensure_templates!
|
45
52
|
|
46
53
|
@created_items = []
|
47
54
|
@failed_templates = []
|
48
55
|
@skipped_templates = []
|
49
56
|
|
50
|
-
|
51
|
-
|
52
|
-
pending_templates
|
53
|
-
|
54
|
-
|
55
|
-
else
|
56
|
-
confirm_workload(template)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
progress.puts if @asked_for_confirmation
|
61
|
-
|
62
|
-
@deprecated_variables = []
|
63
|
-
|
64
|
-
pending_templates.each do |template, filename|
|
65
|
-
step("Applying template '#{template}'", abort_on_error: false) do
|
66
|
-
items = apply_template(filename)
|
67
|
-
unless items
|
68
|
-
report_failure(template)
|
69
|
-
next false
|
70
|
-
end
|
71
|
-
|
72
|
-
items.each do |item|
|
73
|
-
report_success(item)
|
74
|
-
end
|
75
|
-
true
|
76
|
-
end
|
57
|
+
templates = @template_parser.parse(@names_to_filenames.values)
|
58
|
+
pending_templates = confirm_templates(templates)
|
59
|
+
add_app_identity_template(pending_templates) if config.options[:add_app_identity]
|
60
|
+
pending_templates.each do |template|
|
61
|
+
apply_template(template)
|
77
62
|
end
|
78
63
|
|
79
|
-
warn_deprecated_variables
|
80
|
-
|
81
64
|
print_created_items
|
82
65
|
print_failed_templates
|
83
66
|
print_skipped_templates
|
@@ -87,18 +70,21 @@ module Command
|
|
87
70
|
|
88
71
|
private
|
89
72
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
73
|
+
def template_kind(template)
|
74
|
+
case template["kind"]
|
75
|
+
when "gvc"
|
76
|
+
"app"
|
77
|
+
else
|
78
|
+
template["kind"]
|
93
79
|
end
|
94
80
|
end
|
95
81
|
|
96
82
|
def ensure_templates!
|
97
|
-
missing_templates =
|
83
|
+
missing_templates = @names_to_filenames.reject { |_, filename| File.exist?(filename) }
|
98
84
|
return if missing_templates.empty?
|
99
85
|
|
100
|
-
missing_templates_str = missing_templates.map do |
|
101
|
-
" - #{
|
86
|
+
missing_templates_str = missing_templates.map do |name, filename|
|
87
|
+
" - #{name} (#{filename})"
|
102
88
|
end.join("\n")
|
103
89
|
progress.puts("#{Shell.color('Missing templates:', :red)}\n#{missing_templates_str}\n\n")
|
104
90
|
|
@@ -113,10 +99,10 @@ module Command
|
|
113
99
|
end
|
114
100
|
|
115
101
|
def confirm_app(template)
|
116
|
-
app = cp.fetch_gvc
|
102
|
+
app = cp.fetch_gvc(template["name"])
|
117
103
|
return true unless app
|
118
104
|
|
119
|
-
confirmed = confirm_apply("App '#{
|
105
|
+
confirmed = confirm_apply("App '#{template['name']}' already exists, do you want to re-create it?")
|
120
106
|
return true if confirmed
|
121
107
|
|
122
108
|
report_skipped(template)
|
@@ -124,63 +110,67 @@ module Command
|
|
124
110
|
end
|
125
111
|
|
126
112
|
def confirm_workload(template)
|
127
|
-
workload = cp.fetch_workload(template)
|
113
|
+
workload = cp.fetch_workload(template["name"])
|
128
114
|
return true unless workload
|
129
115
|
|
130
|
-
confirmed = confirm_apply("Workload '#{template}' already exists, do you want to re-create it?")
|
116
|
+
confirmed = confirm_apply("Workload '#{template['name']}' already exists, do you want to re-create it?")
|
131
117
|
return true if confirmed
|
132
118
|
|
133
119
|
report_skipped(template)
|
134
120
|
false
|
135
121
|
end
|
136
122
|
|
137
|
-
def
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
123
|
+
def confirm_templates(templates) # rubocop:disable Metrics/MethodLength
|
124
|
+
@asked_for_confirmation = false
|
125
|
+
|
126
|
+
pending_templates = templates.select do |template|
|
127
|
+
case template["kind"]
|
128
|
+
when "gvc"
|
129
|
+
confirm_app(template)
|
130
|
+
when "workload"
|
131
|
+
confirm_workload(template)
|
132
|
+
else
|
133
|
+
true
|
134
|
+
end
|
135
|
+
end
|
149
136
|
|
150
|
-
|
137
|
+
progress.puts if @asked_for_confirmation
|
138
|
+
|
139
|
+
pending_templates
|
140
|
+
end
|
151
141
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
.gsub("APP_GVC", config.app)
|
156
|
-
.gsub("APP_LOCATION", config.location)
|
157
|
-
.gsub("APP_IMAGE", latest_image)
|
142
|
+
def add_app_identity_template(templates)
|
143
|
+
app_template_index = templates.index { |template| template["name"] == config.app }
|
144
|
+
app_identity_template_index = templates.index { |template| template["name"] == config.identity }
|
158
145
|
|
159
|
-
|
160
|
-
|
146
|
+
return unless app_template_index && app_identity_template_index.nil?
|
147
|
+
|
148
|
+
# Adding the identity template right after the app template is important since:
|
149
|
+
# a) we can't create the identity at the beginning because the app doesn't exist yet
|
150
|
+
# b) we also can't create it at the end because any workload templates associated with it will fail to apply
|
151
|
+
templates.insert(app_template_index + 1, build_app_identity_hash)
|
161
152
|
end
|
162
153
|
|
163
|
-
def
|
154
|
+
def build_app_identity_hash
|
164
155
|
{
|
165
|
-
"
|
166
|
-
"
|
167
|
-
"APP_LOCATION" => "{{APP_LOCATION}}",
|
168
|
-
"APP_IMAGE" => "{{APP_IMAGE}}"
|
156
|
+
"kind" => "identity",
|
157
|
+
"name" => config.identity
|
169
158
|
}
|
170
159
|
end
|
171
160
|
|
172
|
-
def
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
161
|
+
def apply_template(template) # rubocop:disable Metrics/MethodLength
|
162
|
+
step("Applying template for #{template_kind(template)} '#{template['name']}'", abort_on_error: false) do
|
163
|
+
items = cp.apply_hash(template)
|
164
|
+
unless items
|
165
|
+
report_failure(template)
|
166
|
+
next false
|
167
|
+
end
|
179
168
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
169
|
+
items.each do |item|
|
170
|
+
report_success(item)
|
171
|
+
end
|
172
|
+
true
|
173
|
+
end
|
184
174
|
end
|
185
175
|
|
186
176
|
def report_success(item)
|
@@ -205,14 +195,14 @@ module Command
|
|
205
195
|
def print_failed_templates
|
206
196
|
return unless @failed_templates.any?
|
207
197
|
|
208
|
-
failed = @failed_templates.map { |template| " - #{template}" }.join("\n")
|
198
|
+
failed = @failed_templates.map { |template| " - [#{template_kind(template)}] #{template['name']}" }.join("\n")
|
209
199
|
progress.puts("\n#{Shell.color('Failed to apply templates:', :red)}\n#{failed}")
|
210
200
|
end
|
211
201
|
|
212
202
|
def print_skipped_templates
|
213
203
|
return unless @skipped_templates.any?
|
214
204
|
|
215
|
-
skipped = @skipped_templates.map { |template| " - #{template}" }.join("\n")
|
205
|
+
skipped = @skipped_templates.map { |template| " - [#{template_kind(template)}] #{template['name']}" }.join("\n")
|
216
206
|
progress.puts("\n#{Shell.color('Skipped templates (already exist):', :blue)}\n#{skipped}")
|
217
207
|
end
|
218
208
|
end
|
data/lib/command/base.rb
CHANGED
@@ -8,6 +8,10 @@ module Command
|
|
8
8
|
|
9
9
|
include Helpers
|
10
10
|
|
11
|
+
VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS = %w[config].freeze
|
12
|
+
VALIDATIONS_WITH_ADDITIONAL_OPTIONS = %w[templates].freeze
|
13
|
+
ALL_VALIDATIONS = VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS + VALIDATIONS_WITH_ADDITIONAL_OPTIONS
|
14
|
+
|
11
15
|
# Used to call the command (`cpl NAME`)
|
12
16
|
# NAME = ""
|
13
17
|
# Displayed when running `cpl help` or `cpl help NAME` (defaults to `NAME`)
|
@@ -32,8 +36,8 @@ module Command
|
|
32
36
|
HIDE = false
|
33
37
|
# Whether or not to show key information like ORG and APP name in commands
|
34
38
|
WITH_INFO_HEADER = true
|
35
|
-
|
36
|
-
|
39
|
+
# Which validations to run before the command
|
40
|
+
VALIDATIONS = %w[config].freeze
|
37
41
|
|
38
42
|
def initialize(config)
|
39
43
|
@config = config
|
@@ -269,6 +273,7 @@ module Command
|
|
269
273
|
def self.skip_secret_access_binding_option(required: false)
|
270
274
|
{
|
271
275
|
name: :skip_secret_access_binding,
|
276
|
+
new_name: :skip_secrets_setup,
|
272
277
|
params: {
|
273
278
|
desc: "Skips secret access binding",
|
274
279
|
type: :boolean,
|
@@ -277,6 +282,17 @@ module Command
|
|
277
282
|
}
|
278
283
|
end
|
279
284
|
|
285
|
+
def self.skip_secrets_setup_option(required: false)
|
286
|
+
{
|
287
|
+
name: :skip_secrets_setup,
|
288
|
+
params: {
|
289
|
+
desc: "Skips secrets setup",
|
290
|
+
type: :boolean,
|
291
|
+
required: required
|
292
|
+
}
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
280
296
|
def self.run_release_phase_option(required: false)
|
281
297
|
{
|
282
298
|
name: :run_release_phase,
|
@@ -378,57 +394,65 @@ module Command
|
|
378
394
|
}
|
379
395
|
}
|
380
396
|
end
|
381
|
-
# rubocop:enable Metrics/MethodLength
|
382
397
|
|
383
|
-
def self.
|
384
|
-
|
398
|
+
def self.validations_option(required: false)
|
399
|
+
{
|
400
|
+
name: :validations,
|
401
|
+
params: {
|
402
|
+
banner: "VALIDATION_1,VALIDATION_2,...",
|
403
|
+
desc: "Which validations to run " \
|
404
|
+
"(must be separated by a comma)",
|
405
|
+
type: :string,
|
406
|
+
required: required,
|
407
|
+
default: VALIDATIONS_WITHOUT_ADDITIONAL_OPTIONS.join(","),
|
408
|
+
valid_regex: /^(#{ALL_VALIDATIONS.join("|")})(,(#{ALL_VALIDATIONS.join("|")}))*$/
|
409
|
+
}
|
410
|
+
}
|
385
411
|
end
|
386
412
|
|
387
|
-
def self.
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
413
|
+
def self.skip_post_creation_hook_option(required: false)
|
414
|
+
{
|
415
|
+
name: :skip_post_creation_hook,
|
416
|
+
params: {
|
417
|
+
desc: "Skips post-creation hook",
|
418
|
+
type: :boolean,
|
419
|
+
required: required
|
420
|
+
}
|
421
|
+
}
|
392
422
|
end
|
393
423
|
|
394
|
-
def
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
end
|
424
|
+
def self.skip_pre_deletion_hook_option(required: false)
|
425
|
+
{
|
426
|
+
name: :skip_pre_deletion_hook,
|
427
|
+
params: {
|
428
|
+
desc: "Skips pre-deletion hook",
|
429
|
+
type: :boolean,
|
430
|
+
required: required
|
431
|
+
}
|
432
|
+
}
|
404
433
|
end
|
405
434
|
|
406
|
-
def
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
435
|
+
def self.add_app_identity_option(required: false)
|
436
|
+
{
|
437
|
+
name: :add_app_identity,
|
438
|
+
params: {
|
439
|
+
desc: "Adds app identity template if it does not exist",
|
440
|
+
type: :boolean,
|
441
|
+
required: required
|
442
|
+
}
|
443
|
+
}
|
414
444
|
end
|
445
|
+
# rubocop:enable Metrics/MethodLength
|
415
446
|
|
416
|
-
def
|
417
|
-
|
418
|
-
commit ||= config.options[:commit]
|
419
|
-
|
420
|
-
@latest_image_next ||= {}
|
421
|
-
@latest_image_next[app] ||= begin
|
422
|
-
latest_image_name = latest_image(app, org)
|
423
|
-
image = latest_image_name.split(":").first
|
424
|
-
image += ":#{extract_image_number(latest_image_name) + 1}"
|
425
|
-
image += "_#{commit}" if commit
|
426
|
-
image
|
427
|
-
end
|
447
|
+
def self.all_options
|
448
|
+
methods.grep(/_option$/).map { |method| send(method.to_s) }
|
428
449
|
end
|
429
450
|
|
430
|
-
def
|
431
|
-
|
451
|
+
def self.all_options_by_key_name
|
452
|
+
all_options.each_with_object({}) do |option, result|
|
453
|
+
option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option }
|
454
|
+
result["--#{option[:name]}"] = option
|
455
|
+
end
|
432
456
|
end
|
433
457
|
|
434
458
|
# NOTE: use simplified variant atm, as shelljoin do different escaping
|
@@ -486,30 +510,6 @@ module Command
|
|
486
510
|
@cp ||= Controlplane.new(config)
|
487
511
|
end
|
488
512
|
|
489
|
-
def app_location_link
|
490
|
-
"/org/#{config.org}/location/#{config.location}"
|
491
|
-
end
|
492
|
-
|
493
|
-
def app_image_link
|
494
|
-
"/org/#{config.org}/image/#{latest_image}"
|
495
|
-
end
|
496
|
-
|
497
|
-
def app_identity
|
498
|
-
"#{config.app}-identity"
|
499
|
-
end
|
500
|
-
|
501
|
-
def app_identity_link
|
502
|
-
"/org/#{config.org}/gvc/#{config.app}/identity/#{app_identity}"
|
503
|
-
end
|
504
|
-
|
505
|
-
def app_secrets
|
506
|
-
config.current[:secrets_name] || "#{config.app_prefix}-secrets"
|
507
|
-
end
|
508
|
-
|
509
|
-
def app_secrets_policy
|
510
|
-
config.current[:secrets_policy_name] || "#{app_secrets}-policy"
|
511
|
-
end
|
512
|
-
|
513
513
|
def ensure_docker_running!
|
514
514
|
result = Shell.cmd("docker", "version", capture_stderr: true)
|
515
515
|
return if result[:success]
|
@@ -517,13 +517,24 @@ module Command
|
|
517
517
|
raise "Can't run Docker. Please make sure that it's installed and started, then try again."
|
518
518
|
end
|
519
519
|
|
520
|
-
|
520
|
+
def run_command_in_latest_image(command, title:)
|
521
|
+
# Need to prefix the command with '.controlplane/'
|
522
|
+
# if it's a file in the '.controlplane' directory,
|
523
|
+
# for backwards compatibility
|
524
|
+
path = Pathname.new("#{config.app_cpln_dir}/#{command}").expand_path
|
525
|
+
command = ".controlplane/#{command}" if File.exist?(path)
|
526
|
+
|
527
|
+
progress.puts("Running #{title}...\n\n")
|
521
528
|
|
522
|
-
|
523
|
-
|
524
|
-
|
529
|
+
begin
|
530
|
+
Cpl::Cli.start(["run", "-a", config.app, "--image", "latest", "--", command])
|
531
|
+
rescue SystemExit => e
|
532
|
+
progress.puts
|
525
533
|
|
526
|
-
|
534
|
+
raise "Failed to run #{title}." if e.status.nonzero?
|
535
|
+
|
536
|
+
progress.puts("Finished running #{title}.\n\n")
|
537
|
+
end
|
527
538
|
end
|
528
539
|
end
|
529
540
|
end
|
data/lib/command/build_image.rb
CHANGED
@@ -27,7 +27,7 @@ module Command
|
|
27
27
|
|
28
28
|
progress.puts("Building image from Dockerfile '#{dockerfile}'...\n\n")
|
29
29
|
|
30
|
-
image_name = latest_image_next
|
30
|
+
image_name = cp.latest_image_next
|
31
31
|
image_url = "#{config.org}.registry.cpln.io/#{image_name}"
|
32
32
|
|
33
33
|
commit = config.options[:commit]
|
@@ -41,7 +41,7 @@ module Command
|
|
41
41
|
progress.puts("\nPushed image to '/org/#{config.org}/image/#{image_name}'.\n\n")
|
42
42
|
|
43
43
|
step("Waiting for image to be available", retry_on_failure: true) do
|
44
|
-
image_name == latest_image(refresh: true)
|
44
|
+
image_name == cp.latest_image(refresh: true)
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
@@ -56,7 +56,7 @@ module Command
|
|
56
56
|
return images unless cp.fetch_gvc(app)
|
57
57
|
|
58
58
|
# If app exists, remove latest image, because we don't want to delete the image that is currently deployed
|
59
|
-
latest_image_name = latest_image_from(images, app_name: app)
|
59
|
+
latest_image_name = cp.latest_image_from(images, app_name: app)
|
60
60
|
images.reject { |image| image["name"] == latest_image_name }
|
61
61
|
end
|
62
62
|
|
@@ -49,7 +49,7 @@ module Command
|
|
49
49
|
app_name = gvc["name"]
|
50
50
|
|
51
51
|
images = cp.query_images(app_name)["items"].select { |item| item["name"].start_with?("#{app_name}:") }
|
52
|
-
image = latest_image_from(images, app_name: app_name, name_only: false)
|
52
|
+
image = cp.latest_image_from(images, app_name: app_name, name_only: false)
|
53
53
|
next unless image
|
54
54
|
|
55
55
|
created_date = DateTime.parse(image["created"])
|
@@ -66,8 +66,8 @@ module Command
|
|
66
66
|
step("Fetching upstream image URL") do
|
67
67
|
cp.profile_switch(@upstream_profile)
|
68
68
|
upstream_image = config.options[:image]
|
69
|
-
upstream_image = latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
|
70
|
-
@commit = extract_image_commit(upstream_image)
|
69
|
+
upstream_image = cp.latest_image(@upstream, @upstream_org) if !upstream_image || upstream_image == "latest"
|
70
|
+
@commit = cp.extract_image_commit(upstream_image)
|
71
71
|
@upstream_image_url = "#{@upstream_org}.registry.cpln.io/#{upstream_image}"
|
72
72
|
end
|
73
73
|
end
|
@@ -75,7 +75,7 @@ module Command
|
|
75
75
|
def fetch_app_image_url
|
76
76
|
step("Fetching app image URL") do
|
77
77
|
cp.profile_switch("default")
|
78
|
-
app_image = latest_image_next(config.app, config.org, commit: @commit)
|
78
|
+
app_image = cp.latest_image_next(config.app, config.org, commit: @commit)
|
79
79
|
@app_image_url = "#{config.org}.registry.cpln.io/#{app_image}"
|
80
80
|
end
|
81
81
|
end
|
data/lib/command/delete.rb
CHANGED
@@ -6,13 +6,17 @@ module Command
|
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
8
|
workload_option,
|
9
|
-
skip_confirm_option
|
9
|
+
skip_confirm_option,
|
10
|
+
skip_pre_deletion_hook_option
|
10
11
|
].freeze
|
11
12
|
DESCRIPTION = "Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload"
|
12
13
|
LONG_DESCRIPTION = <<~DESC
|
13
14
|
- Deletes the whole app (GVC with all workloads, all volumesets and all images) or a specific workload
|
14
15
|
- Also unbinds the app from the secrets policy, as long as both the identity and the policy exist (and are bound)
|
15
16
|
- Will ask for explicit user confirmation
|
17
|
+
- Runs a pre-deletion hook before the app is deleted if `hooks.pre_deletion` is specified in the `.controlplane/controlplane.yml` file
|
18
|
+
- If the hook exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
19
|
+
- Use `--skip-pre-deletion-hook` to skip the hook if specified in `controlplane.yml`
|
16
20
|
DESC
|
17
21
|
EXAMPLES = <<~EX
|
18
22
|
```sh
|
@@ -51,6 +55,7 @@ module Command
|
|
51
55
|
check_images
|
52
56
|
return unless confirm_delete(config.app)
|
53
57
|
|
58
|
+
run_pre_deletion_hook unless config.options[:skip_pre_deletion_hook]
|
54
59
|
unbind_identity_from_policy
|
55
60
|
delete_volumesets
|
56
61
|
delete_gvc
|
@@ -119,19 +124,26 @@ module Command
|
|
119
124
|
end
|
120
125
|
|
121
126
|
def unbind_identity_from_policy
|
122
|
-
return if cp.fetch_identity(
|
127
|
+
return if cp.fetch_identity(config.identity).nil?
|
123
128
|
|
124
|
-
policy = cp.fetch_policy(
|
129
|
+
policy = cp.fetch_policy(config.secrets_policy)
|
125
130
|
return if policy.nil?
|
126
131
|
|
127
132
|
is_bound = policy["bindings"].any? do |binding|
|
128
|
-
binding["principalLinks"].any? { |link| link ==
|
133
|
+
binding["principalLinks"].any? { |link| link == config.identity_link }
|
129
134
|
end
|
130
135
|
return unless is_bound
|
131
136
|
|
132
137
|
step("Unbinding identity from policy for app '#{config.app}'") do
|
133
|
-
cp.unbind_identity_from_policy(
|
138
|
+
cp.unbind_identity_from_policy(config.identity_link, config.secrets_policy)
|
134
139
|
end
|
135
140
|
end
|
141
|
+
|
142
|
+
def run_pre_deletion_hook
|
143
|
+
pre_deletion_hook = config.current.dig(:hooks, :pre_deletion)
|
144
|
+
return unless pre_deletion_hook
|
145
|
+
|
146
|
+
run_command_in_latest_image(pre_deletion_hook, title: "pre-deletion hook")
|
147
|
+
end
|
136
148
|
end
|
137
149
|
end
|