cpl 0.5.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +19 -4
- data/README.md +67 -20
- data/Rakefile +2 -2
- data/cpl.gemspec +3 -0
- data/docs/commands.md +108 -21
- data/examples/circleci.yml +1 -1
- data/examples/controlplane.yml +3 -0
- data/lib/command/{setup.rb → apply_template.rb} +89 -15
- data/lib/command/base.rb +68 -18
- data/lib/command/build_image.rb +15 -3
- data/lib/command/copy_image_from_upstream.rb +1 -1
- data/lib/command/info.rb +9 -5
- data/lib/command/maintenance.rb +37 -0
- data/lib/command/maintenance_off.rb +58 -0
- data/lib/command/maintenance_on.rb +58 -0
- data/lib/command/maintenance_set_page.rb +34 -0
- data/lib/command/no_command.rb +1 -1
- data/lib/command/promote_app_from_upstream.rb +2 -2
- data/lib/command/ps_start.rb +22 -5
- data/lib/command/ps_stop.rb +22 -5
- data/lib/command/run.rb +53 -14
- data/lib/command/run_cleanup.rb +99 -0
- data/lib/command/run_detached.rb +22 -14
- data/lib/command/setup_app.rb +29 -0
- data/lib/core/config.rb +39 -32
- data/lib/core/controlplane.rb +78 -16
- data/lib/core/controlplane_api.rb +39 -0
- data/lib/core/controlplane_api_direct.rb +13 -3
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +6 -1
- data/lib/deprecated_commands.json +2 -1
- data/templates/daily-task.yml +30 -0
- data/templates/maintenance.yml +24 -0
- metadata +54 -4
data/lib/command/base.rb
CHANGED
@@ -142,13 +142,47 @@ module Command
|
|
142
142
|
}
|
143
143
|
end
|
144
144
|
|
145
|
+
def self.use_local_token_option(required: false)
|
146
|
+
{
|
147
|
+
name: :use_local_token,
|
148
|
+
params: {
|
149
|
+
desc: "Override remote CPLN_TOKEN with local token",
|
150
|
+
type: :boolean,
|
151
|
+
required: required
|
152
|
+
}
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.terminal_size_option(required: false)
|
157
|
+
{
|
158
|
+
name: :terminal_size,
|
159
|
+
params: {
|
160
|
+
banner: "ROWS,COLS",
|
161
|
+
desc: "Override remote terminal size (e.g. `--terminal-size 10,20`)",
|
162
|
+
type: :string,
|
163
|
+
required: required
|
164
|
+
}
|
165
|
+
}
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.wait_option(title = "", required: false)
|
169
|
+
{
|
170
|
+
name: :wait,
|
171
|
+
params: {
|
172
|
+
desc: "Waits for #{title}",
|
173
|
+
type: :boolean,
|
174
|
+
required: required
|
175
|
+
}
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
145
179
|
def self.all_options
|
146
180
|
methods.grep(/_option$/).map { |method| send(method.to_s) }
|
147
181
|
end
|
148
182
|
|
149
183
|
def self.all_options_by_key_name
|
150
184
|
all_options.each_with_object({}) do |option, result|
|
151
|
-
option[:params][:aliases]
|
185
|
+
option[:params][:aliases]&.each { |current_alias| result[current_alias.to_s] = option }
|
152
186
|
result["--#{option[:name]}"] = option
|
153
187
|
end
|
154
188
|
end
|
@@ -168,13 +202,13 @@ module Command
|
|
168
202
|
|
169
203
|
def wait_for_replica(workload, location)
|
170
204
|
wait_for("replica") do
|
171
|
-
cp.
|
205
|
+
cp.workload_get_replicas_safely(workload, location: location)&.dig("items", 0)
|
172
206
|
end
|
173
207
|
end
|
174
208
|
|
175
209
|
def ensure_workload_deleted(workload)
|
176
210
|
progress.puts "- Ensure workload is deleted"
|
177
|
-
cp.
|
211
|
+
cp.delete_workload(workload)
|
178
212
|
end
|
179
213
|
|
180
214
|
def latest_image_from(items, app_name: config.app, name_only: true)
|
@@ -201,8 +235,9 @@ module Command
|
|
201
235
|
def latest_image_next(app = config.app, org = config.org)
|
202
236
|
@latest_image_next ||= {}
|
203
237
|
@latest_image_next[app] ||= begin
|
204
|
-
|
205
|
-
image
|
238
|
+
latest_image_name = latest_image(app, org)
|
239
|
+
image = latest_image_name.split(":").first
|
240
|
+
image += ":#{extract_image_number(latest_image_name) + 1}"
|
206
241
|
image += "_#{config.options[:commit]}" if config.options[:commit]
|
207
242
|
image
|
208
243
|
end
|
@@ -218,29 +253,44 @@ module Command
|
|
218
253
|
$stderr
|
219
254
|
end
|
220
255
|
|
221
|
-
def
|
256
|
+
def step_error(error, abort_on_error: true)
|
257
|
+
message = error.message
|
258
|
+
if abort_on_error
|
259
|
+
progress.puts(" #{Shell.color('failed!', :red)}\n\n")
|
260
|
+
Shell.abort(message)
|
261
|
+
else
|
262
|
+
Shell.write_to_tmp_stderr(message)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def step_finish(success)
|
267
|
+
if success
|
268
|
+
progress.puts(" #{Shell.color('done!', :green)}")
|
269
|
+
else
|
270
|
+
progress.puts(" #{Shell.color('failed!', :red)}\n\n#{Shell.read_from_tmp_stderr}\n\n")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def step(message, abort_on_error: true, retry_on_failure: false) # rubocop:disable Metrics/MethodLength
|
222
275
|
progress.print("#{message}...")
|
223
276
|
|
224
277
|
Shell.use_tmp_stderr do
|
225
278
|
success = false
|
226
279
|
|
227
280
|
begin
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
Shell.abort(message)
|
281
|
+
if retry_on_failure
|
282
|
+
until (success = yield)
|
283
|
+
progress.print(".")
|
284
|
+
sleep 1
|
285
|
+
end
|
234
286
|
else
|
235
|
-
|
287
|
+
success = yield
|
236
288
|
end
|
289
|
+
rescue RuntimeError => e
|
290
|
+
step_error(e, abort_on_error: abort_on_error)
|
237
291
|
end
|
238
292
|
|
239
|
-
|
240
|
-
progress.puts(" #{Shell.color('done!', :green)}")
|
241
|
-
else
|
242
|
-
progress.puts(" #{Shell.color('failed!', :red)}\n\n#{Shell.read_from_tmp_stderr}\n\n")
|
243
|
-
end
|
293
|
+
step_finish(success)
|
244
294
|
end
|
245
295
|
end
|
246
296
|
|
data/lib/command/build_image.rb
CHANGED
@@ -11,18 +11,30 @@ module Command
|
|
11
11
|
LONG_DESCRIPTION = <<~DESC
|
12
12
|
- Builds and pushes the image to Control Plane
|
13
13
|
- Automatically assigns image numbers, e.g., `app:1`, `app:2`, etc.
|
14
|
-
- Uses `.controlplane/Dockerfile`
|
14
|
+
- Uses `.controlplane/Dockerfile` or a different Dockerfile specified through `dockerfile` in the `.controlplane/controlplane.yml` file
|
15
|
+
- If a commit is provided through `--commit` or `-c`, it will be set as the runtime env var `GIT_COMMIT`
|
15
16
|
DESC
|
16
17
|
|
17
|
-
def call
|
18
|
+
def call # rubocop:disable Metrics/MethodLength
|
18
19
|
ensure_docker_running!
|
19
20
|
|
20
21
|
dockerfile = config.current[:dockerfile] || "Dockerfile"
|
21
22
|
dockerfile = "#{config.app_cpln_dir}/#{dockerfile}"
|
22
23
|
|
24
|
+
raise "Can't find Dockerfile at '#{dockerfile}'." unless File.exist?(dockerfile)
|
25
|
+
|
23
26
|
progress.puts("Building image from Dockerfile '#{dockerfile}'...\n\n")
|
24
27
|
|
25
|
-
|
28
|
+
image_name = latest_image_next
|
29
|
+
image_url = "#{config.org}.registry.cpln.io/#{image_name}"
|
30
|
+
|
31
|
+
commit = config.options[:commit]
|
32
|
+
build_args = []
|
33
|
+
build_args.push("GIT_COMMIT=#{commit}") if commit
|
34
|
+
|
35
|
+
cp.image_build(image_url, dockerfile: dockerfile, build_args: build_args)
|
36
|
+
|
37
|
+
progress.puts("\nPushed image to '/org/#{config.org}/image/#{image_name}'.")
|
26
38
|
end
|
27
39
|
|
28
40
|
private
|
@@ -29,7 +29,7 @@ module Command
|
|
29
29
|
ensure_docker_running!
|
30
30
|
|
31
31
|
@upstream = config[:upstream]
|
32
|
-
@upstream_org = config.apps[@upstream.to_sym][:cpln_org]
|
32
|
+
@upstream_org = config.apps[@upstream.to_sym][:cpln_org]
|
33
33
|
ensure_upstream_org!
|
34
34
|
|
35
35
|
create_upstream_profile
|
data/lib/command/info.rb
CHANGED
@@ -90,7 +90,7 @@ module Command
|
|
90
90
|
config.apps.each do |app_name, app_options|
|
91
91
|
next if config.app && !app_matches?(config.app, app_name, app_options)
|
92
92
|
|
93
|
-
org = app_options[:cpln_org]
|
93
|
+
org = app_options[:cpln_org]
|
94
94
|
result.push(org) unless result.include?(org)
|
95
95
|
end
|
96
96
|
end
|
@@ -104,7 +104,7 @@ module Command
|
|
104
104
|
config.apps.each do |app_name, app_options|
|
105
105
|
next if config.app && !app_matches?(config.app, app_name, app_options)
|
106
106
|
|
107
|
-
app_org = app_options[:cpln_org]
|
107
|
+
app_org = app_options[:cpln_org]
|
108
108
|
result.push(app_name.to_s) if app_org == org
|
109
109
|
end
|
110
110
|
|
@@ -173,7 +173,11 @@ module Command
|
|
173
173
|
puts "\nSome apps/workloads are missing. Please create them with:"
|
174
174
|
|
175
175
|
@missing_apps_workloads.each do |app, workloads|
|
176
|
-
|
176
|
+
if workloads.include?("gvc")
|
177
|
+
puts " - `cpl setup-app -a #{app}`"
|
178
|
+
else
|
179
|
+
puts " - `cpl apply-template #{workloads.join(' ')} -a #{app}`"
|
180
|
+
end
|
177
181
|
end
|
178
182
|
end
|
179
183
|
|
@@ -183,9 +187,9 @@ module Command
|
|
183
187
|
puts "\nThere are no apps starting with some names. If you wish to create any, do so with " \
|
184
188
|
"(replace 'whatever' with whatever suffix you want):"
|
185
189
|
|
186
|
-
@missing_apps_starting_with.each do |app,
|
190
|
+
@missing_apps_starting_with.each do |app, _workloads|
|
187
191
|
app_with_suffix = "#{app}#{app.end_with?('-') ? '' : '-'}whatever"
|
188
|
-
puts " - `cpl setup
|
192
|
+
puts " - `cpl setup-app -a #{app_with_suffix}`"
|
189
193
|
end
|
190
194
|
end
|
191
195
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Maintenance < Base
|
5
|
+
NAME = "maintenance"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Checks if maintenance mode is on or off for an app"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Checks if maintenance mode is on or off for an app
|
12
|
+
- Outputs 'on' or 'off'
|
13
|
+
- Specify the one-off workload through `one_off_workload` in the `.controlplane/controlplane.yml` file
|
14
|
+
- Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
|
15
|
+
- Maintenance mode is only supported for domains that use path based routing mode and have a route configured for the prefix '/' on either port 80 or 443
|
16
|
+
DESC
|
17
|
+
|
18
|
+
def call # rubocop:disable Metrics/MethodLength
|
19
|
+
one_off_workload = config[:one_off_workload]
|
20
|
+
maintenance_workload = config.current[:maintenance_workload] || "maintenance"
|
21
|
+
|
22
|
+
domain_data = cp.find_domain_for([one_off_workload, maintenance_workload])
|
23
|
+
unless domain_data
|
24
|
+
raise "Can't find domain. " \
|
25
|
+
"Maintenance mode is only supported for domains that use path based routing mode " \
|
26
|
+
"and have a route configured for the prefix '/' on either port 80 or 443."
|
27
|
+
end
|
28
|
+
|
29
|
+
domain_workload = cp.get_domain_workload(domain_data)
|
30
|
+
if domain_workload == maintenance_workload
|
31
|
+
puts "on"
|
32
|
+
else
|
33
|
+
puts "off"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class MaintenanceOff < Base
|
5
|
+
NAME = "maintenance:off"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Disables maintenance mode for an app"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Disables maintenance mode for an app
|
12
|
+
- Specify the one-off workload through `one_off_workload` in the `.controlplane/controlplane.yml` file
|
13
|
+
- Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
|
14
|
+
- Maintenance mode is only supported for domains that use path based routing mode and have a route configured for the prefix '/' on either port 80 or 443
|
15
|
+
DESC
|
16
|
+
|
17
|
+
def call # rubocop:disable Metrics/MethodLength
|
18
|
+
one_off_workload = config[:one_off_workload]
|
19
|
+
maintenance_workload = config.current[:maintenance_workload] || "maintenance"
|
20
|
+
|
21
|
+
domain_data = cp.find_domain_for([one_off_workload, maintenance_workload])
|
22
|
+
unless domain_data
|
23
|
+
raise "Can't find domain. " \
|
24
|
+
"Maintenance mode is only supported for domains that use path based routing mode " \
|
25
|
+
"and have a route configured for the prefix '/' on either port 80 or 443."
|
26
|
+
end
|
27
|
+
|
28
|
+
domain = domain_data["name"]
|
29
|
+
domain_workload = cp.get_domain_workload(domain_data)
|
30
|
+
if domain_workload == one_off_workload
|
31
|
+
progress.puts("Maintenance mode is already disabled for app '#{config.app}'.")
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
cp.fetch_workload!(maintenance_workload)
|
36
|
+
|
37
|
+
# Start all other workloads
|
38
|
+
Cpl::Cli.start(["ps:start", "-a", config.app, "--wait"])
|
39
|
+
|
40
|
+
progress.puts
|
41
|
+
|
42
|
+
# Switch domain workload
|
43
|
+
step("Switching workload for domain '#{domain}' to '#{one_off_workload}'") do
|
44
|
+
cp.set_domain_workload(domain_data, one_off_workload)
|
45
|
+
|
46
|
+
# Give it a bit of time for the domain to update
|
47
|
+
sleep 30
|
48
|
+
end
|
49
|
+
|
50
|
+
progress.puts
|
51
|
+
|
52
|
+
# Stop maintenance workload
|
53
|
+
Cpl::Cli.start(["ps:stop", "-a", config.app, "-w", maintenance_workload, "--wait"])
|
54
|
+
|
55
|
+
progress.puts("\nMaintenance mode disabled for app '#{config.app}'.")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class MaintenanceOn < Base
|
5
|
+
NAME = "maintenance:on"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Enables maintenance mode for an app"
|
10
|
+
LONG_DESCRIPTION = <<~DESC
|
11
|
+
- Enables maintenance mode for an app
|
12
|
+
- Specify the one-off workload through `one_off_workload` in the `.controlplane/controlplane.yml` file
|
13
|
+
- Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
|
14
|
+
- Maintenance mode is only supported for domains that use path based routing mode and have a route configured for the prefix '/' on either port 80 or 443
|
15
|
+
DESC
|
16
|
+
|
17
|
+
def call # rubocop:disable Metrics/MethodLength
|
18
|
+
one_off_workload = config[:one_off_workload]
|
19
|
+
maintenance_workload = config.current[:maintenance_workload] || "maintenance"
|
20
|
+
|
21
|
+
domain_data = cp.find_domain_for([one_off_workload, maintenance_workload])
|
22
|
+
unless domain_data
|
23
|
+
raise "Can't find domain. " \
|
24
|
+
"Maintenance mode is only supported for domains that use path based routing mode " \
|
25
|
+
"and have a route configured for the prefix '/' on either port 80 or 443."
|
26
|
+
end
|
27
|
+
|
28
|
+
domain = domain_data["name"]
|
29
|
+
domain_workload = cp.get_domain_workload(domain_data)
|
30
|
+
if domain_workload == maintenance_workload
|
31
|
+
progress.puts("Maintenance mode is already enabled for app '#{config.app}'.")
|
32
|
+
return
|
33
|
+
end
|
34
|
+
|
35
|
+
cp.fetch_workload!(maintenance_workload)
|
36
|
+
|
37
|
+
# Start maintenance workload
|
38
|
+
Cpl::Cli.start(["ps:start", "-a", config.app, "-w", maintenance_workload, "--wait"])
|
39
|
+
|
40
|
+
progress.puts
|
41
|
+
|
42
|
+
# Switch domain workload
|
43
|
+
step("Switching workload for domain '#{domain}' to '#{maintenance_workload}'") do
|
44
|
+
cp.set_domain_workload(domain_data, maintenance_workload)
|
45
|
+
|
46
|
+
# Give it a bit of time for the domain to update
|
47
|
+
sleep 30
|
48
|
+
end
|
49
|
+
|
50
|
+
progress.puts
|
51
|
+
|
52
|
+
# Stop all other workloads
|
53
|
+
Cpl::Cli.start(["ps:stop", "-a", config.app, "--wait"])
|
54
|
+
|
55
|
+
progress.puts("\nMaintenance mode enabled for app '#{config.app}'.")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class MaintenanceSetPage < Base
|
5
|
+
NAME = "maintenance:set-page"
|
6
|
+
USAGE = "maintenance:set-page PAGE_URL"
|
7
|
+
REQUIRES_ARGS = true
|
8
|
+
OPTIONS = [
|
9
|
+
app_option(required: true)
|
10
|
+
].freeze
|
11
|
+
DESCRIPTION = "Sets the page for maintenance mode"
|
12
|
+
LONG_DESCRIPTION = <<~DESC
|
13
|
+
- Sets the page for maintenance mode
|
14
|
+
- Only works if the maintenance workload uses the `shakacode/maintenance-mode` image
|
15
|
+
- Will set the URL as an env var `PAGE_URL` on the maintenance workload
|
16
|
+
- Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
|
17
|
+
DESC
|
18
|
+
|
19
|
+
def call
|
20
|
+
maintenance_workload = config.current[:maintenance_workload] || "maintenance"
|
21
|
+
|
22
|
+
maintenance_workload_data = cp.fetch_workload!(maintenance_workload)
|
23
|
+
maintenance_workload_data.dig("spec", "containers").each do |container|
|
24
|
+
next unless container["image"].match?(%r{^shakacode/maintenance-mode})
|
25
|
+
|
26
|
+
container_name = container["name"]
|
27
|
+
page_url = config.args.first
|
28
|
+
step("Setting '#{page_url}' as the page for maintenance mode") do
|
29
|
+
cp.set_workload_env_var(maintenance_workload, container: container_name, name: "PAGE_URL", value: page_url)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/command/no_command.rb
CHANGED
@@ -39,7 +39,7 @@ module Command
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def copy_image_from_upstream
|
42
|
-
|
42
|
+
Cpl::Cli.start(["copy-image-from-upstream", "-a", config.app, "-t", config.options[:upstream_token]])
|
43
43
|
progress.puts
|
44
44
|
end
|
45
45
|
|
@@ -52,7 +52,7 @@ module Command
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def deploy_image
|
55
|
-
|
55
|
+
Cpl::Cli.start(["deploy-image", "-a", config.app])
|
56
56
|
end
|
57
57
|
end
|
58
58
|
end
|
data/lib/command/ps_start.rb
CHANGED
@@ -5,7 +5,8 @@ module Command
|
|
5
5
|
NAME = "ps:start"
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
|
-
workload_option
|
8
|
+
workload_option,
|
9
|
+
wait_option("workload to be ready")
|
9
10
|
].freeze
|
10
11
|
DESCRIPTION = "Starts workloads in app"
|
11
12
|
LONG_DESCRIPTION = <<~DESC
|
@@ -22,12 +23,28 @@ module Command
|
|
22
23
|
EX
|
23
24
|
|
24
25
|
def call
|
25
|
-
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
-
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
26
|
+
@workloads = [config.options[:workload]] if config.options[:workload]
|
27
|
+
@workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
28
|
|
28
|
-
workloads.reverse_each do |workload|
|
29
|
+
@workloads.reverse_each do |workload|
|
29
30
|
step("Starting workload '#{workload}'") do
|
30
|
-
cp.
|
31
|
+
cp.set_workload_suspend(workload, false)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
wait_for_ready if config.options[:wait]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def wait_for_ready
|
41
|
+
progress.puts
|
42
|
+
|
43
|
+
@workloads.reverse_each do |workload|
|
44
|
+
step("Waiting for workload '#{workload}' to be ready", retry_on_failure: true) do
|
45
|
+
cp.fetch_workload_deployments(workload)&.dig("items")&.any? do |item|
|
46
|
+
item.dig("status", "ready")
|
47
|
+
end
|
31
48
|
end
|
32
49
|
end
|
33
50
|
end
|
data/lib/command/ps_stop.rb
CHANGED
@@ -5,7 +5,8 @@ module Command
|
|
5
5
|
NAME = "ps:stop"
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
|
-
workload_option
|
8
|
+
workload_option,
|
9
|
+
wait_option("workload to be not ready")
|
9
10
|
].freeze
|
10
11
|
DESCRIPTION = "Stops workloads in app"
|
11
12
|
LONG_DESCRIPTION = <<~DESC
|
@@ -22,12 +23,28 @@ module Command
|
|
22
23
|
EX
|
23
24
|
|
24
25
|
def call
|
25
|
-
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
-
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
26
|
+
@workloads = [config.options[:workload]] if config.options[:workload]
|
27
|
+
@workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
28
|
|
28
|
-
workloads.each do |workload|
|
29
|
+
@workloads.each do |workload|
|
29
30
|
step("Stopping workload '#{workload}'") do
|
30
|
-
cp.
|
31
|
+
cp.set_workload_suspend(workload, true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
wait_for_not_ready if config.options[:wait]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def wait_for_not_ready
|
41
|
+
progress.puts
|
42
|
+
|
43
|
+
@workloads.each do |workload|
|
44
|
+
step("Waiting for workload '#{workload}' to be not ready", retry_on_failure: true) do
|
45
|
+
cp.fetch_workload_deployments(workload)&.dig("items")&.all? do |item|
|
46
|
+
!item.dig("status", "ready")
|
47
|
+
end
|
31
48
|
end
|
32
49
|
end
|
33
50
|
end
|
data/lib/command/run.rb
CHANGED
@@ -8,13 +8,17 @@ module Command
|
|
8
8
|
DEFAULT_ARGS = ["bash"].freeze
|
9
9
|
OPTIONS = [
|
10
10
|
app_option(required: true),
|
11
|
-
image_option
|
11
|
+
image_option,
|
12
|
+
workload_option,
|
13
|
+
use_local_token_option,
|
14
|
+
terminal_size_option
|
12
15
|
].freeze
|
13
16
|
DESCRIPTION = "Runs one-off **_interactive_** replicas (analog of `heroku run`)"
|
14
17
|
LONG_DESCRIPTION = <<~DESC
|
15
18
|
- Runs one-off **_interactive_** replicas (analog of `heroku run`)
|
16
19
|
- Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
|
17
20
|
- May not work correctly with tasks that last over 5 minutes (there's a Control Plane scaling bug at the moment)
|
21
|
+
- If `fix_terminal_size` is `true` in the `.controlplane/controlplane.yml` file, the remote terminal size will be fixed to match the local terminal size (may also be overriden through `--terminal-size`)
|
18
22
|
|
19
23
|
> **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
|
20
24
|
> task crashing are tolerable. For production tasks, it's better to use `cpl run:detached`.
|
@@ -24,6 +28,12 @@ module Command
|
|
24
28
|
# Opens shell (bash by default).
|
25
29
|
cpl run -a $APP_NAME
|
26
30
|
|
31
|
+
# Need to quote COMMAND if setting ENV value or passing args.
|
32
|
+
cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
|
33
|
+
|
34
|
+
# COMMAND may also be passed at the end (in this case, no need to quote).
|
35
|
+
cpl run -a $APP_NAME -- rails db:migrate
|
36
|
+
|
27
37
|
# Runs command, displays output, and exits shell.
|
28
38
|
cpl run ls / -a $APP_NAME
|
29
39
|
cpl run rails db:migrate:status -a $APP_NAME
|
@@ -34,19 +44,25 @@ module Command
|
|
34
44
|
# Uses a different image (which may not be promoted yet).
|
35
45
|
cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
|
36
46
|
cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
|
47
|
+
|
48
|
+
# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
|
49
|
+
cpl run bash -a $APP_NAME -w other-workload
|
50
|
+
|
51
|
+
# Overrides remote CPLN_TOKEN env variable with local token.
|
52
|
+
# Useful when superuser rights are needed in remote container.
|
53
|
+
cpl run bash -a $APP_NAME --use-local-token
|
37
54
|
```
|
38
55
|
EX
|
39
56
|
|
40
|
-
attr_reader :location, :workload, :one_off
|
57
|
+
attr_reader :location, :workload, :one_off, :container
|
41
58
|
|
42
59
|
def call
|
43
60
|
@location = config[:default_location]
|
44
|
-
@workload = config[:one_off_workload]
|
61
|
+
@workload = config.options["workload"] || config[:one_off_workload]
|
45
62
|
@one_off = "#{workload}-run-#{rand(1000..9999)}"
|
46
63
|
|
47
64
|
clone_workload
|
48
65
|
wait_for_workload(one_off)
|
49
|
-
sleep 2 # sometimes replica query lags workload creation, despite ok by prev query
|
50
66
|
wait_for_replica(one_off, location)
|
51
67
|
run_in_replica
|
52
68
|
ensure
|
@@ -60,39 +76,62 @@ module Command
|
|
60
76
|
|
61
77
|
# Create a base copy of workload props
|
62
78
|
spec = cp.fetch_workload!(workload).fetch("spec")
|
63
|
-
|
79
|
+
container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
|
80
|
+
@container = container_spec["name"]
|
64
81
|
|
65
82
|
# remove other containers if any
|
66
|
-
spec["containers"] = [
|
83
|
+
spec["containers"] = [container_spec]
|
67
84
|
|
68
85
|
# Stub workload command with dummy server that just responds to port
|
69
86
|
# Needed to avoid execution of ENTRYPOINT and CMD of Dockerfile
|
70
|
-
|
71
|
-
|
87
|
+
container_spec["command"] = "ruby"
|
88
|
+
container_spec["args"] = ["-e", Scripts.http_dummy_server_ruby]
|
72
89
|
|
73
90
|
# Ensure one-off workload will be running
|
74
91
|
spec["defaultOptions"]["suspend"] = false
|
75
92
|
|
76
93
|
# Ensure no scaling
|
77
94
|
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
78
|
-
spec["defaultOptions"]["autoscaling"]["
|
95
|
+
spec["defaultOptions"]["autoscaling"]["maxScale"] = 1
|
79
96
|
spec["defaultOptions"]["capacityAI"] = false
|
80
97
|
|
81
98
|
# Override image if specified
|
82
99
|
image = config.options[:image]
|
83
100
|
image = "/org/#{config.org}/image/#{latest_image}" if image == "latest"
|
84
|
-
|
101
|
+
container_spec["image"] = image if image
|
85
102
|
|
86
103
|
# Set runner
|
87
|
-
|
88
|
-
|
104
|
+
container_spec["env"] ||= []
|
105
|
+
container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
|
106
|
+
|
107
|
+
if config.options["use_local_token"]
|
108
|
+
container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN", "value" => ControlplaneApiDirect.new.api_token }
|
109
|
+
end
|
89
110
|
|
90
111
|
# Create workload clone
|
91
112
|
cp.apply("kind" => "workload", "name" => one_off, "spec" => spec)
|
92
113
|
end
|
93
114
|
|
94
|
-
def runner_script
|
115
|
+
def runner_script # rubocop:disable Metrics/MethodLength
|
95
116
|
script = Scripts.helpers_cleanup
|
117
|
+
|
118
|
+
if config.options["use_local_token"]
|
119
|
+
script += <<~SHELL
|
120
|
+
CPLN_TOKEN=$CONTROLPLANE_TOKEN
|
121
|
+
unset CONTROLPLANE_TOKEN
|
122
|
+
SHELL
|
123
|
+
end
|
124
|
+
|
125
|
+
# NOTE: fixes terminal size to match local terminal
|
126
|
+
if config.current[:fix_terminal_size] || config.options[:terminal_size]
|
127
|
+
if config.options[:terminal_size]
|
128
|
+
rows, cols = config.options[:terminal_size].split(",")
|
129
|
+
else
|
130
|
+
rows, cols = `stty -a`.match(/(\d+)\s*rows;\s*(\d+)\s*columns/).captures
|
131
|
+
end
|
132
|
+
script += "stty rows #{rows}\nstty cols #{cols}\n" if rows && cols
|
133
|
+
end
|
134
|
+
|
96
135
|
script += args_join(config.args)
|
97
136
|
script
|
98
137
|
end
|
@@ -100,7 +139,7 @@ module Command
|
|
100
139
|
def run_in_replica
|
101
140
|
progress.puts "- Connecting"
|
102
141
|
command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
|
103
|
-
cp.workload_exec(one_off, location: location, container:
|
142
|
+
cp.workload_exec(one_off, location: location, container: container, command: command)
|
104
143
|
end
|
105
144
|
end
|
106
145
|
end
|