cpl 1.3.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 +28 -1
- data/CONTRIBUTING.md +32 -2
- data/Gemfile.lock +38 -29
- data/README.md +43 -17
- data/cpl.gemspec +2 -1
- data/docs/commands.md +68 -59
- data/docs/dns.md +6 -0
- data/docs/migrating.md +10 -10
- data/docs/tips.md +15 -3
- data/examples/circleci.yml +3 -3
- data/examples/controlplane.yml +35 -9
- data/lib/command/apply_template.rb +66 -18
- data/lib/command/base.rb +168 -27
- data/lib/command/build_image.rb +4 -9
- data/lib/command/cleanup_stale_apps.rb +1 -3
- data/lib/command/copy_image_from_upstream.rb +0 -7
- data/lib/command/delete.rb +39 -7
- data/lib/command/deploy_image.rb +35 -2
- 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/promote_app_from_upstream.rb +5 -25
- 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 -68
- data/lib/command/setup_app.rb +22 -2
- data/lib/constants/exit_code.rb +7 -0
- data/lib/core/config.rb +11 -3
- data/lib/core/controlplane.rb +126 -47
- data/lib/core/controlplane_api.rb +15 -1
- data/lib/core/controlplane_api_cli.rb +3 -3
- data/lib/core/controlplane_api_direct.rb +33 -5
- data/lib/core/shell.rb +15 -9
- data/lib/cpl/version.rb +1 -1
- data/lib/cpl.rb +50 -9
- data/lib/deprecated_commands.json +2 -1
- data/lib/generator_templates/controlplane.yml +5 -0
- data/lib/generator_templates/templates/{gvc.yml → app.yml} +4 -4
- data/lib/generator_templates/templates/postgres.yml +1 -1
- data/lib/generator_templates/templates/rails.yml +1 -1
- data/script/check_cpln_links +3 -3
- data/templates/app.yml +18 -0
- data/templates/daily-task.yml +3 -2
- data/templates/rails.yml +3 -2
- data/templates/secrets.yml +11 -0
- data/templates/sidekiq.yml +3 -2
- metadata +38 -25
- data/.rspec +0 -1
- data/lib/command/run_cleanup.rb +0 -116
- data/lib/command/run_detached.rb +0 -175
- data/lib/core/scripts.rb +0 -34
- data/templates/gvc.yml +0 -13
- data/templates/identity.yml +0 -2
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,97 +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
|
-
|
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)
|
108
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)
|
109
184
|
|
110
|
-
|
111
|
-
|
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
|
112
203
|
end
|
113
204
|
|
114
|
-
def
|
115
|
-
|
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
|
116
242
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
SHELL
|
243
|
+
def run_non_interactive
|
244
|
+
if detached
|
245
|
+
print_detached_commands
|
246
|
+
exit(ExitCode::SUCCESS)
|
122
247
|
end
|
123
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
|
+
|
124
362
|
# NOTE: fixes terminal size to match local terminal
|
125
363
|
if config.current[:fix_terminal_size] || config.options[:terminal_size]
|
126
364
|
if config.options[:terminal_size]
|
127
365
|
rows, cols = config.options[:terminal_size].split(",")
|
128
366
|
else
|
367
|
+
# NOTE: cannot use `Shell.cmd` here, as `stty size` has to run in a terminal environment
|
129
368
|
rows, cols = `stty size`.split(/\s+/)
|
130
369
|
end
|
131
|
-
script += "stty rows #{rows}\nstty cols #{cols}\n"
|
370
|
+
script += "stty rows #{rows}\nstty cols #{cols}\n"
|
132
371
|
end
|
133
372
|
|
134
|
-
script += args_join(config.args)
|
135
373
|
script
|
136
374
|
end
|
137
375
|
|
138
|
-
def
|
139
|
-
|
140
|
-
|
141
|
-
|
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 }
|
142
504
|
end
|
143
505
|
end
|
144
506
|
end
|
data/lib/command/setup_app.rb
CHANGED
@@ -4,16 +4,19 @@ module Command
|
|
4
4
|
class SetupApp < Base
|
5
5
|
NAME = "setup-app"
|
6
6
|
OPTIONS = [
|
7
|
-
app_option(required: true)
|
7
|
+
app_option(required: true),
|
8
|
+
skip_secret_access_binding_option
|
8
9
|
].freeze
|
9
10
|
DESCRIPTION = "Creates an app and all its workloads"
|
10
11
|
LONG_DESCRIPTION = <<~DESC
|
11
12
|
- Creates an app and all its workloads
|
12
13
|
- Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
|
13
14
|
- This should only be used for temporary apps like review apps, never for persistent apps like production (to update workloads for those, use 'cpl apply-template' instead)
|
15
|
+
- Automatically binds the app to the secrets policy, as long as both the identity and the policy exist
|
16
|
+
- Use `--skip-secret-access-binding` to prevent the automatic bind
|
14
17
|
DESC
|
15
18
|
|
16
|
-
def call
|
19
|
+
def call # rubocop:disable Metrics/MethodLength
|
17
20
|
templates = config[:setup_app_templates]
|
18
21
|
|
19
22
|
app = cp.fetch_gvc
|
@@ -24,6 +27,23 @@ module Command
|
|
24
27
|
end
|
25
28
|
|
26
29
|
Cpl::Cli.start(["apply-template", *templates, "-a", config.app])
|
30
|
+
|
31
|
+
return if config.options[:skip_secret_access_binding]
|
32
|
+
|
33
|
+
progress.puts
|
34
|
+
|
35
|
+
if cp.fetch_identity(app_identity).nil? || cp.fetch_policy(app_secrets_policy).nil?
|
36
|
+
raise "Can't bind identity to policy: identity '#{app_identity}' or " \
|
37
|
+
"policy '#{app_secrets_policy}' doesn't exist. " \
|
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."
|
42
|
+
end
|
43
|
+
|
44
|
+
step("Binding identity to policy") do
|
45
|
+
cp.bind_identity_to_policy(app_identity_link, app_secrets_policy)
|
46
|
+
end
|
27
47
|
end
|
28
48
|
end
|
29
49
|
end
|
data/lib/core/config.rb
CHANGED
@@ -34,6 +34,10 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
34
34
|
@app ||= load_app_from_options || load_app_from_env
|
35
35
|
end
|
36
36
|
|
37
|
+
def app_prefix
|
38
|
+
current&.fetch(:name)
|
39
|
+
end
|
40
|
+
|
37
41
|
def location
|
38
42
|
@location ||= load_location_from_options || load_location_from_env || load_location_from_file
|
39
43
|
end
|
@@ -111,8 +115,12 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
111
115
|
|
112
116
|
def find_app_config(app_name1)
|
113
117
|
@app_configs ||= {}
|
114
|
-
|
115
|
-
|
118
|
+
|
119
|
+
@app_configs[app_name1] ||= apps.filter_map do |app_name2, app_config|
|
120
|
+
next unless app_matches?(app_name1, app_name2, app_config)
|
121
|
+
|
122
|
+
app_config[:name] = app_name2
|
123
|
+
app_config
|
116
124
|
end&.last
|
117
125
|
end
|
118
126
|
|
@@ -166,7 +174,7 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
166
174
|
end
|
167
175
|
|
168
176
|
def config_file_path # rubocop:disable Metrics/MethodLength
|
169
|
-
@config_file_path ||= begin
|
177
|
+
@config_file_path ||= ENV["CONFIG_FILE_PATH"] || begin
|
170
178
|
path = Pathname.new(".").expand_path
|
171
179
|
|
172
180
|
loop do
|