cpl 1.4.0 → 2.0.0.rc.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 +1 -1
- data/.github/workflows/rspec-shared.yml +56 -0
- data/.github/workflows/rspec.yml +19 -31
- data/.github/workflows/rubocop.yml +2 -10
- data/.gitignore +2 -0
- data/.simplecov_spawn.rb +10 -0
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +32 -2
- data/Gemfile.lock +34 -29
- data/README.md +34 -25
- data/cpl.gemspec +1 -1
- data/docs/commands.md +54 -54
- data/docs/dns.md +6 -0
- data/docs/migrating.md +10 -10
- data/docs/tips.md +12 -10
- data/examples/circleci.yml +3 -3
- data/examples/controlplane.yml +25 -16
- data/lib/command/apply_template.rb +9 -9
- data/lib/command/base.rb +132 -37
- data/lib/command/build_image.rb +4 -9
- data/lib/command/cleanup_stale_apps.rb +1 -1
- data/lib/command/copy_image_from_upstream.rb +0 -7
- data/lib/command/delete.rb +39 -7
- data/lib/command/deploy_image.rb +18 -3
- data/lib/command/exists.rb +1 -1
- data/lib/command/generate.rb +1 -1
- data/lib/command/info.rb +7 -3
- data/lib/command/logs.rb +22 -2
- data/lib/command/maintenance_off.rb +1 -1
- data/lib/command/maintenance_on.rb +1 -1
- data/lib/command/open.rb +2 -2
- data/lib/command/open_console.rb +2 -2
- data/lib/command/ps.rb +1 -1
- data/lib/command/ps_start.rb +2 -1
- data/lib/command/ps_stop.rb +40 -8
- data/lib/command/ps_wait.rb +3 -2
- data/lib/command/run.rb +430 -69
- data/lib/command/setup_app.rb +4 -1
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +1 -1
- data/lib/core/controlplane.rb +109 -48
- data/lib/core/controlplane_api.rb +7 -1
- data/lib/core/controlplane_api_cli.rb +3 -3
- data/lib/core/controlplane_api_direct.rb +1 -1
- data/lib/core/shell.rb +15 -9
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +48 -9
- data/lib/deprecated_commands.json +2 -1
- data/lib/generator_templates/controlplane.yml +2 -2
- data/script/check_cpln_links +3 -3
- data/templates/{gvc.yml → app.yml} +5 -0
- data/templates/secrets.yml +8 -0
- metadata +23 -26
- data/.rspec +0 -1
- data/lib/command/run_cleanup.rb +0 -116
- data/lib/command/run_detached.rb +0 -176
- data/lib/core/scripts.rb +0 -34
- data/templates/identity.yml +0 -3
- data/templates/secrets-policy.yml +0 -4
- /data/lib/generator_templates/templates/{gvc.yml → app.yml} +0 -0
data/lib/command/ps_stop.rb
CHANGED
@@ -6,6 +6,8 @@ module Command
|
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
8
|
workload_option,
|
9
|
+
replica_option,
|
10
|
+
location_option,
|
9
11
|
wait_option("workload to not be ready")
|
10
12
|
].freeze
|
11
13
|
DESCRIPTION = "Stops workloads in app"
|
@@ -19,32 +21,62 @@ module Command
|
|
19
21
|
|
20
22
|
# Stops a specific workload in app.
|
21
23
|
cpl ps:stop -a $APP_NAME -w $WORKLOAD_NAME
|
24
|
+
|
25
|
+
# Stops a specific replica of a workload.
|
26
|
+
cpl ps:stop -a $APP_NAME -w $WORKLOAD_NAME -r $REPLICA_NAME
|
22
27
|
```
|
23
28
|
EX
|
24
29
|
|
25
30
|
def call
|
26
|
-
|
27
|
-
|
31
|
+
workload = config.options[:workload]
|
32
|
+
replica = config.options[:replica]
|
33
|
+
if replica
|
34
|
+
stop_replica(workload, replica)
|
35
|
+
else
|
36
|
+
workloads = [workload] if workload
|
37
|
+
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
38
|
+
|
39
|
+
stop_workloads(workloads)
|
40
|
+
end
|
41
|
+
end
|
28
42
|
|
29
|
-
|
43
|
+
private
|
44
|
+
|
45
|
+
def stop_workloads(workloads)
|
46
|
+
workloads.each do |workload|
|
30
47
|
step("Stopping workload '#{workload}'") do
|
31
48
|
cp.set_workload_suspend(workload, true)
|
32
49
|
end
|
33
50
|
end
|
34
51
|
|
35
|
-
|
52
|
+
wait_for_workloads_not_ready(workloads) if config.options[:wait]
|
36
53
|
end
|
37
54
|
|
38
|
-
|
55
|
+
def stop_replica(workload, replica)
|
56
|
+
step("Stopping replica '#{replica}'", retry_on_failure: true) do
|
57
|
+
cp.stop_workload_replica(workload, replica, location: config.location)
|
58
|
+
end
|
39
59
|
|
40
|
-
|
60
|
+
wait_for_replica_not_ready(workload, replica) if config.options[:wait]
|
61
|
+
end
|
62
|
+
|
63
|
+
def wait_for_workloads_not_ready(workloads)
|
41
64
|
progress.puts
|
42
65
|
|
43
|
-
|
66
|
+
workloads.each do |workload|
|
44
67
|
step("Waiting for workload '#{workload}' to not be ready", retry_on_failure: true) do
|
45
|
-
cp.workload_deployments_ready?(workload, expected_status: false)
|
68
|
+
cp.workload_deployments_ready?(workload, location: config.location, expected_status: false)
|
46
69
|
end
|
47
70
|
end
|
48
71
|
end
|
72
|
+
|
73
|
+
def wait_for_replica_not_ready(workload, replica)
|
74
|
+
progress.puts
|
75
|
+
|
76
|
+
step("Waiting for replica '#{replica}' to not be ready", retry_on_failure: true) do
|
77
|
+
result = cp.fetch_workload_replicas(workload, location: config.location)
|
78
|
+
!result["items"].include?(replica)
|
79
|
+
end
|
80
|
+
end
|
49
81
|
end
|
50
82
|
end
|
data/lib/command/ps_wait.rb
CHANGED
@@ -5,7 +5,8 @@ module Command
|
|
5
5
|
NAME = "ps:wait"
|
6
6
|
OPTIONS = [
|
7
7
|
app_option(required: true),
|
8
|
-
workload_option
|
8
|
+
workload_option,
|
9
|
+
location_option
|
9
10
|
].freeze
|
10
11
|
DESCRIPTION = "Waits for workloads in app to be ready after re-deployment"
|
11
12
|
LONG_DESCRIPTION = <<~DESC
|
@@ -27,7 +28,7 @@ module Command
|
|
27
28
|
|
28
29
|
@workloads.reverse_each do |workload|
|
29
30
|
step("Waiting for workload '#{workload}' to be ready", retry_on_failure: true) do
|
30
|
-
cp.workload_deployments_ready?(workload, expected_status: true)
|
31
|
+
cp.workload_deployments_ready?(workload, location: config.location, expected_status: true)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
data/lib/command/run.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Command
|
4
|
-
class Run < Base
|
4
|
+
class Run < Base # rubocop:disable Metrics/ClassLength
|
5
|
+
INTERACTIVE_COMMANDS = [
|
6
|
+
"bash",
|
7
|
+
"rails console",
|
8
|
+
"rails c",
|
9
|
+
"rails dbconsole",
|
10
|
+
"rails db"
|
11
|
+
].freeze
|
12
|
+
|
5
13
|
NAME = "run"
|
6
14
|
USAGE = "run COMMAND"
|
7
15
|
REQUIRES_ARGS = true
|
@@ -9,34 +17,53 @@ module Command
|
|
9
17
|
OPTIONS = [
|
10
18
|
app_option(required: true),
|
11
19
|
image_option,
|
20
|
+
log_method_option,
|
12
21
|
workload_option,
|
13
22
|
location_option,
|
14
23
|
use_local_token_option,
|
15
|
-
terminal_size_option
|
24
|
+
terminal_size_option,
|
25
|
+
interactive_option,
|
26
|
+
detached_option,
|
27
|
+
cpu_option,
|
28
|
+
memory_option,
|
29
|
+
entrypoint_option
|
16
30
|
].freeze
|
17
|
-
DESCRIPTION = "Runs one-off
|
31
|
+
DESCRIPTION = "Runs one-off interactive or non-interactive replicas (analog of `heroku run`)"
|
18
32
|
LONG_DESCRIPTION = <<~DESC
|
19
|
-
- Runs one-off
|
20
|
-
- Uses `
|
21
|
-
-
|
22
|
-
|
23
|
-
|
24
|
-
|
33
|
+
- Runs one-off interactive or non-interactive replicas (analog of `heroku run`)
|
34
|
+
- Uses `Cron` workload type and either:
|
35
|
+
- - `cpln workload exec` for interactive mode, with CLI streaming
|
36
|
+
- - log async fetching for non-interactive mode
|
37
|
+
- The Dockerfile entrypoint is used as the command by default, which assumes `exec "${@}"` to be present,
|
38
|
+
and the args ["bash", "-c", cmd_to_run] are passed
|
39
|
+
- The entrypoint can be overriden through `--entrypoint`, which must be a single command or a script path that exists in the container,
|
40
|
+
and the args ["bash", "-c", cmd_to_run] are passed,
|
41
|
+
unless the entrypoint is `bash`, in which case the args ["-c", cmd_to_run] are passed
|
42
|
+
- Providing `--entrypoint none` sets the entrypoint to `bash` by default
|
43
|
+
- If `fix_terminal_size` is `true` in the `.controlplane/controlplane.yml` file,
|
44
|
+
the remote terminal size will be fixed to match the local terminal size (may also be overriden through `--terminal-size`)
|
25
45
|
DESC
|
26
46
|
EXAMPLES = <<~EX
|
27
47
|
```sh
|
28
48
|
# Opens shell (bash by default).
|
29
49
|
cpl run -a $APP_NAME
|
30
50
|
|
31
|
-
#
|
32
|
-
cpl run -a $APP_NAME --
|
51
|
+
# Runs interactive command, keeps shell open, and stops job when exiting.
|
52
|
+
cpl run -a $APP_NAME --interactive -- rails c
|
53
|
+
|
54
|
+
# Some commands are automatically detected as interactive, so no need to pass `--interactive`.
|
55
|
+
#{INTERACTIVE_COMMANDS.map { |cmd| "cpl run -a $APP_NAME -- #{cmd}" }.join("\n ")}
|
33
56
|
|
34
|
-
# Runs command,
|
35
|
-
cpl run -a $APP_NAME --
|
36
|
-
cpl run -a $APP_NAME -- rails db:migrate:status
|
57
|
+
# Runs non-interactive command, outputs logs, exits with the exit code of the command and stops job.
|
58
|
+
cpl run -a $APP_NAME -- rails db:migrate
|
37
59
|
|
38
|
-
# Runs command and
|
39
|
-
|
60
|
+
# Runs non-iteractive command, detaches, exits with 0, and prints commands to:
|
61
|
+
# - see logs from the job
|
62
|
+
# - stop the job
|
63
|
+
cpl run -a $APP_NAME --detached -- rails db:migrate
|
64
|
+
|
65
|
+
# The command needs to be quoted if setting an env variable or passing args.
|
66
|
+
cpl run -a $APP_NAME -- 'SOME_ENV_VAR=some_value rails db:migrate'
|
40
67
|
|
41
68
|
# Uses a different image (which may not be promoted yet).
|
42
69
|
cpl run -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
|
@@ -48,98 +75,432 @@ module Command
|
|
48
75
|
# Overrides remote CPLN_TOKEN env variable with local token.
|
49
76
|
# Useful when superuser rights are needed in remote container.
|
50
77
|
cpl run -a $APP_NAME --use-local-token -- bash
|
78
|
+
|
79
|
+
# Replaces the existing Dockerfile entrypoint with `bash`.
|
80
|
+
cpl run -a $APP_NAME --entrypoint none -- rails db:migrate
|
81
|
+
|
82
|
+
# Replaces the existing Dockerfile entrypoint.
|
83
|
+
cpl run -a $APP_NAME --entrypoint /app/alternative-entrypoint.sh -- rails db:migrate
|
51
84
|
```
|
52
85
|
EX
|
53
86
|
|
54
|
-
|
87
|
+
MAGIC_END = "---cpl run command finished---"
|
88
|
+
|
89
|
+
attr_reader :interactive, :detached, :location, :original_workload, :runner_workload,
|
90
|
+
:container, :image_link, :image_changed, :job, :replica, :command
|
91
|
+
|
92
|
+
def call # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
93
|
+
@interactive = config.options[:interactive] || interactive_command?
|
94
|
+
@detached = config.options[:detached]
|
95
|
+
@log_method = config.options[:log_method]
|
55
96
|
|
56
|
-
def call # rubocop:disable Metrics/MethodLength
|
57
97
|
@location = config.location
|
58
|
-
@
|
59
|
-
@
|
98
|
+
@original_workload = config.options[:workload] || config[:one_off_workload]
|
99
|
+
@runner_workload = "#{original_workload}-runner"
|
100
|
+
|
101
|
+
unless interactive
|
102
|
+
@internal_sigint = false
|
103
|
+
|
104
|
+
# Catch Ctrl+C in the main process
|
105
|
+
trap("SIGINT") do
|
106
|
+
unless @internal_sigint
|
107
|
+
print_detached_commands
|
108
|
+
exit(ExitCode::INTERRUPT)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
60
112
|
|
61
|
-
|
62
|
-
|
113
|
+
if cp.fetch_workload(runner_workload).nil?
|
114
|
+
create_runner_workload
|
115
|
+
wait_for_runner_workload_create
|
63
116
|
end
|
117
|
+
update_runner_workload
|
118
|
+
wait_for_runner_workload_update
|
119
|
+
|
120
|
+
# NOTE: need to wait some time before starting the job,
|
121
|
+
# otherwise the image may not be updated yet
|
122
|
+
# TODO: need to figure out if there's a better way to do this
|
123
|
+
sleep 1 if image_changed
|
124
|
+
|
125
|
+
start_job
|
126
|
+
wait_for_replica_for_job
|
64
127
|
|
65
|
-
wait_for_workload(workload_clone)
|
66
|
-
wait_for_replica(workload_clone, location)
|
67
|
-
run_in_replica
|
68
|
-
ensure
|
69
128
|
progress.puts
|
70
|
-
|
129
|
+
if interactive
|
130
|
+
run_interactive
|
131
|
+
else
|
132
|
+
run_non_interactive
|
133
|
+
end
|
71
134
|
end
|
72
135
|
|
73
136
|
private
|
74
137
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
138
|
+
def interactive_command?
|
139
|
+
INTERACTIVE_COMMANDS.include?(args_join(config.args))
|
140
|
+
end
|
141
|
+
|
142
|
+
def app_workload_replica_args
|
143
|
+
["-a", config.app, "--workload", runner_workload, "--replica", replica]
|
144
|
+
end
|
80
145
|
|
81
|
-
|
82
|
-
|
146
|
+
def create_runner_workload # rubocop:disable Metrics/MethodLength
|
147
|
+
step("Creating runner workload '#{runner_workload}' based on '#{original_workload}'") do
|
148
|
+
spec, container_spec = base_workload_specs(original_workload)
|
83
149
|
|
84
|
-
|
85
|
-
|
86
|
-
container_spec["command"] = "ruby"
|
87
|
-
container_spec["args"] = ["-e", Scripts.http_dummy_server_ruby]
|
150
|
+
# Remove other containers if any
|
151
|
+
spec["containers"] = [container_spec]
|
88
152
|
|
89
|
-
|
90
|
-
|
153
|
+
# Default to using existing Dockerfile entrypoint
|
154
|
+
container_spec.delete("command")
|
91
155
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
156
|
+
# Remove props that conflict with job
|
157
|
+
container_spec.delete("ports")
|
158
|
+
container_spec.delete("lifecycle")
|
159
|
+
container_spec.delete("livenessProbe")
|
160
|
+
container_spec.delete("readinessProbe")
|
96
161
|
|
97
|
-
|
98
|
-
|
99
|
-
image = latest_image if image == "latest"
|
100
|
-
container_spec["image"] = "/org/#{config.org}/image/#{image}" if image
|
162
|
+
# Ensure cron workload won't run per schedule
|
163
|
+
spec["defaultOptions"]["suspend"] = true
|
101
164
|
|
102
|
-
|
103
|
-
|
104
|
-
|
165
|
+
# Ensure no scaling
|
166
|
+
spec["defaultOptions"]["autoscaling"] = {}
|
167
|
+
spec["defaultOptions"]["capacityAI"] = false
|
105
168
|
|
106
|
-
|
107
|
-
|
108
|
-
|
169
|
+
# Set cron job props
|
170
|
+
spec["type"] = "cron"
|
171
|
+
|
172
|
+
# Next job set to run on January 1st, 2029
|
173
|
+
spec["job"] = { "schedule" => "0 0 1 1 1", "restartPolicy" => "Never" }
|
174
|
+
|
175
|
+
# Create runner workload
|
176
|
+
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
109
177
|
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def update_runner_workload # rubocop:disable Metrics/MethodLength
|
181
|
+
step("Updating runner workload '#{runner_workload}' based on '#{original_workload}'") do
|
182
|
+
_, original_container_spec = base_workload_specs(original_workload)
|
183
|
+
spec, container_spec = base_workload_specs(runner_workload)
|
110
184
|
|
111
|
-
|
112
|
-
|
185
|
+
# Override image if specified
|
186
|
+
image = config.options[:image]
|
187
|
+
if image
|
188
|
+
image = latest_image if image == "latest"
|
189
|
+
@image_link = "/org/#{config.org}/image/#{image}"
|
190
|
+
else
|
191
|
+
@image_link = original_container_spec["image"]
|
192
|
+
end
|
193
|
+
@image_changed = container_spec["image"] != image_link
|
194
|
+
container_spec["image"] = image_link
|
195
|
+
|
196
|
+
# Container overrides
|
197
|
+
container_spec["cpu"] = config.options[:cpu] if config.options[:cpu]
|
198
|
+
container_spec["memory"] = config.options[:memory] if config.options[:memory]
|
199
|
+
|
200
|
+
# Update runner workload
|
201
|
+
cp.apply_hash("kind" => "workload", "name" => runner_workload, "spec" => spec)
|
202
|
+
end
|
113
203
|
end
|
114
204
|
|
115
|
-
def
|
116
|
-
|
205
|
+
def wait_for_runner_workload_create
|
206
|
+
step("Waiting for runner workload '#{runner_workload}' to be created", retry_on_failure: true) do
|
207
|
+
cp.fetch_workload(runner_workload)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def wait_for_runner_workload_update
|
212
|
+
step("Waiting for runner workload '#{runner_workload}' to be updated", retry_on_failure: true) do
|
213
|
+
_, container_spec = base_workload_specs(runner_workload)
|
214
|
+
container_spec["image"] == image_link
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def start_job
|
219
|
+
job_start_yaml = build_job_start_yaml
|
220
|
+
|
221
|
+
step("Starting job for runner workload '#{runner_workload}'", retry_on_failure: true) do
|
222
|
+
result = cp.start_cron_workload(runner_workload, job_start_yaml, location: location)
|
223
|
+
@job = result&.dig("items", 0, "id")
|
224
|
+
|
225
|
+
job || false
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def wait_for_replica_for_job
|
230
|
+
step("Waiting for replica to start, which runs job '#{job}'", retry_on_failure: true) do
|
231
|
+
result = cp.fetch_workload_replicas(runner_workload, location: location)
|
232
|
+
@replica = result["items"].find { |item| item.include?(job) }
|
233
|
+
|
234
|
+
replica || false
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def run_interactive
|
239
|
+
progress.puts("Connecting to replica '#{replica}'...\n\n")
|
240
|
+
cp.workload_exec(runner_workload, replica, location: location, container: container, command: command)
|
241
|
+
end
|
117
242
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
SHELL
|
243
|
+
def run_non_interactive
|
244
|
+
if detached
|
245
|
+
print_detached_commands
|
246
|
+
exit(ExitCode::SUCCESS)
|
123
247
|
end
|
124
248
|
|
249
|
+
case @log_method
|
250
|
+
when 1 then run_non_interactive_v1
|
251
|
+
when 2 then run_non_interactive_v2
|
252
|
+
when 3 then run_non_interactive_v3
|
253
|
+
else raise "Invalid log method: #{@log_method}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def run_non_interactive_v1 # rubocop:disable Metrics/MethodLength
|
258
|
+
logs_pid = Process.fork do
|
259
|
+
# Catch Ctrl+C in the forked process
|
260
|
+
trap("SIGINT") do
|
261
|
+
exit(ExitCode::SUCCESS)
|
262
|
+
end
|
263
|
+
|
264
|
+
Cpl::Cli.start(["logs", *app_workload_replica_args])
|
265
|
+
end
|
266
|
+
Process.detach(logs_pid)
|
267
|
+
|
268
|
+
exit_status = wait_for_job_status
|
269
|
+
|
270
|
+
# We need to wait a bit for the logs to appear,
|
271
|
+
# otherwise it may exit without showing them
|
272
|
+
Kernel.sleep(30)
|
273
|
+
|
274
|
+
@internal_sigint = true
|
275
|
+
Process.kill("INT", logs_pid)
|
276
|
+
exit(exit_status)
|
277
|
+
end
|
278
|
+
|
279
|
+
def run_non_interactive_v2
|
280
|
+
current_cpl = File.expand_path("cpl", "#{__dir__}/../..")
|
281
|
+
logs_pipe = IO.popen([current_cpl, "logs", *app_workload_replica_args])
|
282
|
+
|
283
|
+
exit_status = wait_for_job_status_and_log(logs_pipe)
|
284
|
+
|
285
|
+
@internal_sigint = true
|
286
|
+
Process.kill("INT", logs_pipe.pid)
|
287
|
+
exit(exit_status)
|
288
|
+
end
|
289
|
+
|
290
|
+
def run_non_interactive_v3
|
291
|
+
exit(show_logs_waiting)
|
292
|
+
end
|
293
|
+
|
294
|
+
def base_workload_specs(workload)
|
295
|
+
spec = cp.fetch_workload!(workload).fetch("spec")
|
296
|
+
container_spec = spec["containers"].detect { _1["name"] == original_workload } || spec["containers"].first
|
297
|
+
@container = container_spec["name"]
|
298
|
+
|
299
|
+
[spec, container_spec]
|
300
|
+
end
|
301
|
+
|
302
|
+
def build_job_start_yaml # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
303
|
+
job_start_hash = { "name" => container }
|
304
|
+
|
305
|
+
if config.options[:use_local_token]
|
306
|
+
job_start_hash["env"] ||= []
|
307
|
+
job_start_hash["env"].push({ "name" => "CPL_TOKEN", "value" => ControlplaneApiDirect.new.api_token[:token] })
|
308
|
+
end
|
309
|
+
|
310
|
+
entrypoint = nil
|
311
|
+
if config.options[:entrypoint]
|
312
|
+
entrypoint = config.options[:entrypoint] == "none" ? "bash" : config.options[:entrypoint]
|
313
|
+
end
|
314
|
+
|
315
|
+
job_start_hash["command"] = entrypoint if entrypoint
|
316
|
+
job_start_hash["args"] ||= []
|
317
|
+
job_start_hash["args"].push("bash") unless entrypoint == "bash"
|
318
|
+
job_start_hash["args"].push("-c")
|
319
|
+
job_start_hash["env"] ||= []
|
320
|
+
job_start_hash["env"].push({ "name" => "CPL_RUNNER_SCRIPT", "value" => runner_script })
|
321
|
+
if interactive
|
322
|
+
job_start_hash["env"].push({ "name" => "CPL_MONITORING_SCRIPT", "value" => interactive_monitoring_script })
|
323
|
+
|
324
|
+
job_start_hash["args"].push('eval "$CPL_MONITORING_SCRIPT"')
|
325
|
+
@command = %(bash -c 'eval "$CPL_RUNNER_SCRIPT"')
|
326
|
+
else
|
327
|
+
job_start_hash["args"].push('eval "$CPL_RUNNER_SCRIPT"')
|
328
|
+
end
|
329
|
+
|
330
|
+
job_start_hash.to_yaml
|
331
|
+
end
|
332
|
+
|
333
|
+
def interactive_monitoring_script
|
334
|
+
<<~SCRIPT
|
335
|
+
primary_pid=""
|
336
|
+
|
337
|
+
check_primary() {
|
338
|
+
if ! kill -0 $primary_pid 2>/dev/null; then
|
339
|
+
echo "Primary process has exited. Shutting down."
|
340
|
+
exit 0
|
341
|
+
fi
|
342
|
+
}
|
343
|
+
|
344
|
+
while true; do
|
345
|
+
if [[ -z "$primary_pid" ]]; then
|
346
|
+
primary_pid=$(ps -eo pid,etime,cmd --sort=etime | grep -v "$$" | grep -v 'ps -eo' | grep -v 'grep' | grep 'CPL_RUNNER_SCRIPT' | head -n 1 | awk '{print $1}')
|
347
|
+
if [[ ! -z "$primary_pid" ]]; then
|
348
|
+
echo "Primary process set with PID: $primary_pid"
|
349
|
+
fi
|
350
|
+
else
|
351
|
+
check_primary
|
352
|
+
fi
|
353
|
+
|
354
|
+
sleep 1
|
355
|
+
done
|
356
|
+
SCRIPT
|
357
|
+
end
|
358
|
+
|
359
|
+
def interactive_runner_script
|
360
|
+
script = ""
|
361
|
+
|
125
362
|
# NOTE: fixes terminal size to match local terminal
|
126
363
|
if config.current[:fix_terminal_size] || config.options[:terminal_size]
|
127
364
|
if config.options[:terminal_size]
|
128
365
|
rows, cols = config.options[:terminal_size].split(",")
|
129
366
|
else
|
367
|
+
# NOTE: cannot use `Shell.cmd` here, as `stty size` has to run in a terminal environment
|
130
368
|
rows, cols = `stty size`.split(/\s+/)
|
131
369
|
end
|
132
|
-
script += "stty rows #{rows}\nstty cols #{cols}\n"
|
370
|
+
script += "stty rows #{rows}\nstty cols #{cols}\n"
|
133
371
|
end
|
134
372
|
|
135
|
-
script += args_join(config.args)
|
136
373
|
script
|
137
374
|
end
|
138
375
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
376
|
+
def runner_script # rubocop:disable Metrics/MethodLength
|
377
|
+
script = <<~SCRIPT
|
378
|
+
unset CPL_RUNNER_SCRIPT
|
379
|
+
unset CPL_MONITORING_SCRIPT
|
380
|
+
|
381
|
+
if [ -n "$CPL_TOKEN" ]; then
|
382
|
+
CPLN_TOKEN=$CPL_TOKEN
|
383
|
+
unset CPL_TOKEN
|
384
|
+
fi
|
385
|
+
SCRIPT
|
386
|
+
|
387
|
+
script += interactive_runner_script if interactive
|
388
|
+
|
389
|
+
script +=
|
390
|
+
if @log_method == 1
|
391
|
+
args_join(config.args)
|
392
|
+
else
|
393
|
+
<<~SCRIPT
|
394
|
+
( #{args_join(config.args)} )
|
395
|
+
CPL_EXIT_CODE=$?
|
396
|
+
echo '#{MAGIC_END}'
|
397
|
+
exit $CPL_EXIT_CODE
|
398
|
+
SCRIPT
|
399
|
+
end
|
400
|
+
|
401
|
+
script
|
402
|
+
end
|
403
|
+
|
404
|
+
def wait_for_job_status
|
405
|
+
Kernel.sleep(1) until (exit_code = resolve_job_status)
|
406
|
+
exit_code
|
407
|
+
end
|
408
|
+
|
409
|
+
def wait_for_job_status_and_log(logs_pipe) # rubocop:disable Metrics/MethodLength
|
410
|
+
no_logs_counter = 0
|
411
|
+
|
412
|
+
loop do
|
413
|
+
no_logs_counter += 1
|
414
|
+
break if no_logs_counter > 60 # 30s
|
415
|
+
break if logs_pipe.eof?
|
416
|
+
next Kernel.sleep(0.5) unless logs_pipe.ready?
|
417
|
+
|
418
|
+
no_logs_counter = 0
|
419
|
+
line = logs_pipe.gets
|
420
|
+
break if line.chomp == MAGIC_END
|
421
|
+
|
422
|
+
puts(line)
|
423
|
+
end
|
424
|
+
|
425
|
+
resolve_job_status
|
426
|
+
end
|
427
|
+
|
428
|
+
def print_detached_commands
|
429
|
+
app_workload_replica_config = app_workload_replica_args.join(" ")
|
430
|
+
progress.puts(
|
431
|
+
"\n\n" \
|
432
|
+
"- To view logs from the job, run:\n `cpl logs #{app_workload_replica_config}`\n" \
|
433
|
+
"- To stop the job, run:\n `cpl ps:stop #{app_workload_replica_config}`\n"
|
434
|
+
)
|
435
|
+
end
|
436
|
+
|
437
|
+
def resolve_job_status
|
438
|
+
result = cp.fetch_cron_workload(runner_workload, location: location)
|
439
|
+
job_details = result&.dig("items")&.find { |item| item["id"] == job }
|
440
|
+
status = job_details&.dig("status")
|
441
|
+
|
442
|
+
case status
|
443
|
+
when "failed"
|
444
|
+
ExitCode::ERROR_DEFAULT
|
445
|
+
when "successful"
|
446
|
+
ExitCode::SUCCESS
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
###########################################
|
451
|
+
### temporary extaction from run:detached
|
452
|
+
###########################################
|
453
|
+
def show_logs_waiting # rubocop:disable Metrics/MethodLength
|
454
|
+
retries = 0
|
455
|
+
begin
|
456
|
+
job_finished_count = 0
|
457
|
+
loop do
|
458
|
+
case print_uniq_logs
|
459
|
+
when :finished
|
460
|
+
break
|
461
|
+
when :changed
|
462
|
+
next
|
463
|
+
else
|
464
|
+
job_finished_count += 1 if resolve_job_status
|
465
|
+
break if job_finished_count > 5
|
466
|
+
|
467
|
+
sleep(1)
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
resolve_job_status
|
472
|
+
rescue RuntimeError => e
|
473
|
+
raise "#{e} Exiting..." unless retries < 10 # MAX_RETRIES
|
474
|
+
|
475
|
+
progress.puts(Shell.color("ERROR: #{e} Retrying...", :red))
|
476
|
+
retries += 1
|
477
|
+
retry
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def print_uniq_logs
|
482
|
+
status = nil
|
483
|
+
|
484
|
+
@printed_log_entries ||= []
|
485
|
+
ts = Time.now.to_i
|
486
|
+
entries = normalized_log_entries(from: ts - 60, to: ts)
|
487
|
+
|
488
|
+
(entries - @printed_log_entries).sort.each do |(_ts, val)|
|
489
|
+
status ||= :changed
|
490
|
+
val.chomp == MAGIC_END ? status = :finished : progress.puts(val)
|
491
|
+
end
|
492
|
+
|
493
|
+
@printed_log_entries = entries # as well truncate old entries if any
|
494
|
+
|
495
|
+
status || :unchanged
|
496
|
+
end
|
497
|
+
|
498
|
+
def normalized_log_entries(from:, to:)
|
499
|
+
log = cp.log_get(workload: runner_workload, from: from, to: to, replica: replica)
|
500
|
+
|
501
|
+
log["data"]["result"]
|
502
|
+
.each_with_object([]) { |obj, result| result.concat(obj["values"]) }
|
503
|
+
.select { |ts, _val| ts[..-10].to_i > from }
|
143
504
|
end
|
144
505
|
end
|
145
506
|
end
|
data/lib/command/setup_app.rb
CHANGED
@@ -35,7 +35,10 @@ module Command
|
|
35
35
|
if cp.fetch_identity(app_identity).nil? || cp.fetch_policy(app_secrets_policy).nil?
|
36
36
|
raise "Can't bind identity to policy: identity '#{app_identity}' or " \
|
37
37
|
"policy '#{app_secrets_policy}' doesn't exist. " \
|
38
|
-
"Please create them or use `--skip-secret-access-binding` to ignore this message."
|
38
|
+
"Please create them or use `--skip-secret-access-binding` to ignore this message." \
|
39
|
+
"You can also set a custom secrets name with `secrets_name` " \
|
40
|
+
"and a custom secrets policy name with `secrets_policy_name` " \
|
41
|
+
"in the `.controlplane/controlplane.yml` file."
|
39
42
|
end
|
40
43
|
|
41
44
|
step("Binding identity to policy") do
|