cpl 0.6.0 → 1.0.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/.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
|