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