cpl 0.6.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/command_docs.yml +24 -0
- data/.github/workflows/rspec.yml +8 -5
- data/.github/workflows/rubocop.yml +5 -2
- data/.overcommit.yml +3 -0
- data/.rubocop.yml +6 -0
- data/Gemfile.lock +18 -3
- data/README.md +67 -20
- data/Rakefile +9 -4
- data/cpl.gemspec +3 -0
- data/docs/commands.md +72 -5
- data/examples/circleci.yml +1 -1
- data/examples/controlplane.yml +3 -0
- data/lib/command/base.rb +61 -28
- 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 +30 -10
- data/lib/command/run_cleanup.rb +99 -0
- data/lib/command/run_detached.rb +18 -11
- data/lib/command/setup_app.rb +3 -3
- data/lib/core/config.rb +39 -32
- data/lib/core/controlplane.rb +72 -16
- data/lib/core/controlplane_api.rb +39 -0
- data/lib/core/controlplane_api_direct.rb +13 -3
- data/lib/core/scripts.rb +2 -2
- data/lib/cpl/version.rb +1 -1
- data/script/check_command_docs +3 -0
- data/templates/daily-task.yml +30 -0
- data/templates/maintenance.yml +24 -0
- metadata +54 -3
- /data/script/{generate_commands_docs → update_command_docs} +0 -0
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
@@ -10,13 +10,15 @@ module Command
|
|
10
10
|
app_option(required: true),
|
11
11
|
image_option,
|
12
12
|
workload_option,
|
13
|
-
use_local_token_option
|
13
|
+
use_local_token_option,
|
14
|
+
terminal_size_option
|
14
15
|
].freeze
|
15
16
|
DESCRIPTION = "Runs one-off **_interactive_** replicas (analog of `heroku run`)"
|
16
17
|
LONG_DESCRIPTION = <<~DESC
|
17
18
|
- Runs one-off **_interactive_** replicas (analog of `heroku run`)
|
18
19
|
- Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
|
19
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`)
|
20
22
|
|
21
23
|
> **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
|
22
24
|
> task crashing are tolerable. For production tasks, it's better to use `cpl run:detached`.
|
@@ -26,6 +28,12 @@ module Command
|
|
26
28
|
# Opens shell (bash by default).
|
27
29
|
cpl run -a $APP_NAME
|
28
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
|
+
|
29
37
|
# Runs command, displays output, and exits shell.
|
30
38
|
cpl run ls / -a $APP_NAME
|
31
39
|
cpl run rails db:migrate:status -a $APP_NAME
|
@@ -37,35 +45,37 @@ module Command
|
|
37
45
|
cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
|
38
46
|
cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
|
39
47
|
|
40
|
-
# Uses a different workload
|
48
|
+
# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
|
41
49
|
cpl run bash -a $APP_NAME -w other-workload
|
42
50
|
|
43
51
|
# Overrides remote CPLN_TOKEN env variable with local token.
|
44
|
-
# Useful when
|
52
|
+
# Useful when superuser rights are needed in remote container.
|
45
53
|
cpl run bash -a $APP_NAME --use-local-token
|
46
54
|
```
|
47
55
|
EX
|
48
56
|
|
49
57
|
attr_reader :location, :workload, :one_off, :container
|
50
58
|
|
51
|
-
def call
|
59
|
+
def call # rubocop:disable Metrics/MethodLength
|
52
60
|
@location = config[:default_location]
|
53
61
|
@workload = config.options["workload"] || config[:one_off_workload]
|
54
62
|
@one_off = "#{workload}-run-#{rand(1000..9999)}"
|
55
63
|
|
56
|
-
|
64
|
+
step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
|
65
|
+
clone_workload
|
66
|
+
end
|
67
|
+
|
57
68
|
wait_for_workload(one_off)
|
58
69
|
wait_for_replica(one_off, location)
|
59
70
|
run_in_replica
|
60
71
|
ensure
|
72
|
+
progress.puts
|
61
73
|
ensure_workload_deleted(one_off)
|
62
74
|
end
|
63
75
|
|
64
76
|
private
|
65
77
|
|
66
78
|
def clone_workload # rubocop:disable Metrics/MethodLength
|
67
|
-
progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
|
68
|
-
|
69
79
|
# Create a base copy of workload props
|
70
80
|
spec = cp.fetch_workload!(workload).fetch("spec")
|
71
81
|
container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
|
@@ -84,7 +94,7 @@ module Command
|
|
84
94
|
|
85
95
|
# Ensure no scaling
|
86
96
|
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
87
|
-
spec["defaultOptions"]["autoscaling"]["
|
97
|
+
spec["defaultOptions"]["autoscaling"]["maxScale"] = 1
|
88
98
|
spec["defaultOptions"]["capacityAI"] = false
|
89
99
|
|
90
100
|
# Override image if specified
|
@@ -104,7 +114,7 @@ module Command
|
|
104
114
|
cp.apply("kind" => "workload", "name" => one_off, "spec" => spec)
|
105
115
|
end
|
106
116
|
|
107
|
-
def runner_script
|
117
|
+
def runner_script # rubocop:disable Metrics/MethodLength
|
108
118
|
script = Scripts.helpers_cleanup
|
109
119
|
|
110
120
|
if config.options["use_local_token"]
|
@@ -114,12 +124,22 @@ module Command
|
|
114
124
|
SHELL
|
115
125
|
end
|
116
126
|
|
127
|
+
# NOTE: fixes terminal size to match local terminal
|
128
|
+
if config.current[:fix_terminal_size] || config.options[:terminal_size]
|
129
|
+
if config.options[:terminal_size]
|
130
|
+
rows, cols = config.options[:terminal_size].split(",")
|
131
|
+
else
|
132
|
+
rows, cols = `stty size`.split(/\s+/)
|
133
|
+
end
|
134
|
+
script += "stty rows #{rows}\nstty cols #{cols}\n" if rows && cols
|
135
|
+
end
|
136
|
+
|
117
137
|
script += args_join(config.args)
|
118
138
|
script
|
119
139
|
end
|
120
140
|
|
121
141
|
def run_in_replica
|
122
|
-
progress.puts
|
142
|
+
progress.puts("Connecting...\n\n")
|
123
143
|
command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
|
124
144
|
cp.workload_exec(one_off, location: location, container: container, command: command)
|
125
145
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class RunCleanup < Base
|
5
|
+
NAME = "run:cleanup"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
skip_confirm_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Deletes stale run workloads for an app"
|
11
|
+
LONG_DESCRIPTION = <<~DESC
|
12
|
+
- Deletes stale run workloads for an app
|
13
|
+
- Workloads are considered stale based on how many days since created
|
14
|
+
- `stale_run_workload_created_days` in the `.controlplane/controlplane.yml` file specifies the number of days after created that the workload is considered stale
|
15
|
+
- Will ask for explicit user confirmation of deletion
|
16
|
+
DESC
|
17
|
+
|
18
|
+
def call # rubocop:disable Metrics/MethodLength
|
19
|
+
return progress.puts("No stale run workloads found.") if stale_run_workloads.empty?
|
20
|
+
|
21
|
+
progress.puts("Stale run workloads:")
|
22
|
+
stale_run_workloads.each do |workload|
|
23
|
+
progress.puts(" #{workload[:name]} " \
|
24
|
+
"(#{Shell.color("#{workload[:date]} - #{workload[:days]} days ago", :red)})")
|
25
|
+
end
|
26
|
+
|
27
|
+
return unless confirm_delete
|
28
|
+
|
29
|
+
progress.puts
|
30
|
+
stale_run_workloads.each do |workload|
|
31
|
+
delete_workload(workload)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def app_matches?(app, app_name, app_options)
|
38
|
+
app == app_name.to_s || (app_options[:match_if_app_name_starts_with] && app.start_with?(app_name.to_s))
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_app_options(app)
|
42
|
+
@app_options ||= {}
|
43
|
+
@app_options[app] ||= config.apps.find do |app_name, app_options|
|
44
|
+
app_matches?(app, app_name, app_options)
|
45
|
+
end&.last
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_workloads(app)
|
49
|
+
app_options = find_app_options(app)
|
50
|
+
return [] if app_options.nil?
|
51
|
+
|
52
|
+
(app_options[:app_workloads] + app_options[:additional_workloads] + [app_options[:one_off_workload]]).uniq
|
53
|
+
end
|
54
|
+
|
55
|
+
def stale_run_workloads # rubocop:disable Metrics/MethodLength
|
56
|
+
@stale_run_workloads ||=
|
57
|
+
begin
|
58
|
+
defined_workloads = find_workloads(config.app)
|
59
|
+
|
60
|
+
run_workloads = []
|
61
|
+
|
62
|
+
now = DateTime.now
|
63
|
+
stale_run_workload_created_days = config[:stale_run_workload_created_days]
|
64
|
+
|
65
|
+
workloads = cp.query_workloads("-run-", partial_match: true)["items"]
|
66
|
+
workloads.each do |workload|
|
67
|
+
workload_name = workload["name"]
|
68
|
+
|
69
|
+
original_workload_name, workload_number = workload_name.split("-run-")
|
70
|
+
next unless defined_workloads.include?(original_workload_name) && workload_number.match?(/^\d{4}$/)
|
71
|
+
|
72
|
+
created_date = DateTime.parse(workload["created"])
|
73
|
+
diff_in_days = (now - created_date).to_i
|
74
|
+
next unless diff_in_days >= stale_run_workload_created_days
|
75
|
+
|
76
|
+
run_workloads.push({
|
77
|
+
name: workload_name,
|
78
|
+
date: created_date,
|
79
|
+
days: diff_in_days
|
80
|
+
})
|
81
|
+
end
|
82
|
+
|
83
|
+
run_workloads
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def confirm_delete
|
88
|
+
return true if config.options[:yes]
|
89
|
+
|
90
|
+
Shell.confirm("\nAre you sure you want to delete these #{stale_run_workloads.length} run workloads?")
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete_workload(workload)
|
94
|
+
step("Deleting run workload '#{workload[:name]}'") do
|
95
|
+
cp.delete_workload(workload[:name])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/command/run_detached.rb
CHANGED
@@ -22,9 +22,12 @@ module Command
|
|
22
22
|
```sh
|
23
23
|
cpl run:detached rails db:prepare -a $APP_NAME
|
24
24
|
|
25
|
-
# Need to quote COMMAND if setting ENV value or passing args
|
25
|
+
# Need to quote COMMAND if setting ENV value or passing args.
|
26
26
|
cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
|
27
27
|
|
28
|
+
# COMMAND may also be passed at the end (in this case, no need to quote).
|
29
|
+
cpl run:detached -a $APP_NAME -- rails db:migrate
|
30
|
+
|
28
31
|
# Uses some other image.
|
29
32
|
cpl run:detached rails db:migrate -a $APP_NAME --image /some/full/image/path
|
30
33
|
|
@@ -35,7 +38,7 @@ module Command
|
|
35
38
|
cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
|
36
39
|
cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
|
37
40
|
|
38
|
-
# Uses a different workload
|
41
|
+
# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
|
39
42
|
cpl run:detached rails db:migrate:status -a $APP_NAME -w other-workload
|
40
43
|
```
|
41
44
|
EX
|
@@ -44,24 +47,28 @@ module Command
|
|
44
47
|
|
45
48
|
attr_reader :location, :workload, :one_off, :container
|
46
49
|
|
47
|
-
def call
|
50
|
+
def call # rubocop:disable Metrics/MethodLength
|
48
51
|
@location = config[:default_location]
|
49
52
|
@workload = config.options["workload"] || config[:one_off_workload]
|
50
53
|
@one_off = "#{workload}-runner-#{rand(1000..9999)}"
|
51
54
|
|
52
|
-
|
55
|
+
step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
|
56
|
+
clone_workload
|
57
|
+
end
|
58
|
+
|
53
59
|
wait_for_workload(one_off)
|
54
60
|
show_logs_waiting
|
55
61
|
ensure
|
56
|
-
|
62
|
+
if cp.fetch_workload(one_off)
|
63
|
+
progress.puts
|
64
|
+
ensure_workload_deleted(one_off)
|
65
|
+
end
|
57
66
|
exit(1) if @crashed
|
58
67
|
end
|
59
68
|
|
60
69
|
private
|
61
70
|
|
62
71
|
def clone_workload # rubocop:disable Metrics/MethodLength
|
63
|
-
progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
|
64
|
-
|
65
72
|
# Get base specs of workload
|
66
73
|
spec = cp.fetch_workload!(workload).fetch("spec")
|
67
74
|
container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
|
@@ -79,7 +86,7 @@ module Command
|
|
79
86
|
|
80
87
|
# Ensure no scaling
|
81
88
|
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
82
|
-
spec["defaultOptions"]["autoscaling"]["
|
89
|
+
spec["defaultOptions"]["autoscaling"]["maxScale"] = 1
|
83
90
|
spec["defaultOptions"]["capacityAI"] = false
|
84
91
|
|
85
92
|
# Override image if specified
|
@@ -118,17 +125,17 @@ module Command
|
|
118
125
|
end
|
119
126
|
|
120
127
|
def show_logs_waiting # rubocop:disable Metrics/MethodLength
|
121
|
-
progress.puts
|
128
|
+
progress.puts("Scheduled, fetching logs (it's a cron job, so it may take up to a minute to start)...\n\n")
|
122
129
|
begin
|
123
130
|
while cp.fetch_workload(one_off)
|
124
131
|
sleep(WORKLOAD_SLEEP_CHECK)
|
125
132
|
print_uniq_logs
|
126
133
|
end
|
127
134
|
rescue RuntimeError => e
|
128
|
-
progress.puts
|
135
|
+
progress.puts(Shell.color("ERROR: #{e}", :red))
|
129
136
|
retry
|
130
137
|
end
|
131
|
-
progress.puts
|
138
|
+
progress.puts("\nFinished workload and logger.")
|
132
139
|
end
|
133
140
|
|
134
141
|
def print_uniq_logs
|
data/lib/command/setup_app.rb
CHANGED
@@ -14,16 +14,16 @@ module Command
|
|
14
14
|
DESC
|
15
15
|
|
16
16
|
def call
|
17
|
-
templates = config[:setup]
|
17
|
+
templates = config[:setup]
|
18
18
|
|
19
19
|
app = cp.fetch_gvc
|
20
20
|
if app
|
21
21
|
raise "App '#{config.app}' already exists. If you want to update this app, " \
|
22
22
|
"either run 'cpl delete -a #{config.app}' and then re-run this command, " \
|
23
|
-
"or run 'cpl apply-template #{templates} -a #{config.app}'."
|
23
|
+
"or run 'cpl apply-template #{templates.join(' ')} -a #{config.app}'."
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
Cpl::Cli.start(["apply-template", *templates, "-a", config.app])
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|