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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/command_docs.yml +1 -1
  3. data/.github/workflows/rspec-shared.yml +56 -0
  4. data/.github/workflows/rspec.yml +19 -31
  5. data/.github/workflows/rubocop.yml +2 -10
  6. data/.gitignore +2 -0
  7. data/.simplecov_spawn.rb +10 -0
  8. data/CHANGELOG.md +28 -1
  9. data/CONTRIBUTING.md +32 -2
  10. data/Gemfile.lock +38 -29
  11. data/README.md +43 -17
  12. data/cpl.gemspec +2 -1
  13. data/docs/commands.md +68 -59
  14. data/docs/dns.md +6 -0
  15. data/docs/migrating.md +10 -10
  16. data/docs/tips.md +15 -3
  17. data/examples/circleci.yml +3 -3
  18. data/examples/controlplane.yml +35 -9
  19. data/lib/command/apply_template.rb +66 -18
  20. data/lib/command/base.rb +168 -27
  21. data/lib/command/build_image.rb +4 -9
  22. data/lib/command/cleanup_stale_apps.rb +1 -3
  23. data/lib/command/copy_image_from_upstream.rb +0 -7
  24. data/lib/command/delete.rb +39 -7
  25. data/lib/command/deploy_image.rb +35 -2
  26. data/lib/command/exists.rb +1 -1
  27. data/lib/command/generate.rb +1 -1
  28. data/lib/command/info.rb +7 -3
  29. data/lib/command/logs.rb +22 -2
  30. data/lib/command/maintenance_off.rb +1 -1
  31. data/lib/command/maintenance_on.rb +1 -1
  32. data/lib/command/open.rb +2 -2
  33. data/lib/command/open_console.rb +2 -2
  34. data/lib/command/promote_app_from_upstream.rb +5 -25
  35. data/lib/command/ps.rb +1 -1
  36. data/lib/command/ps_start.rb +2 -1
  37. data/lib/command/ps_stop.rb +40 -8
  38. data/lib/command/ps_wait.rb +3 -2
  39. data/lib/command/run.rb +430 -68
  40. data/lib/command/setup_app.rb +22 -2
  41. data/lib/constants/exit_code.rb +7 -0
  42. data/lib/core/config.rb +11 -3
  43. data/lib/core/controlplane.rb +126 -47
  44. data/lib/core/controlplane_api.rb +15 -1
  45. data/lib/core/controlplane_api_cli.rb +3 -3
  46. data/lib/core/controlplane_api_direct.rb +33 -5
  47. data/lib/core/shell.rb +15 -9
  48. data/lib/cpl/version.rb +1 -1
  49. data/lib/cpl.rb +50 -9
  50. data/lib/deprecated_commands.json +2 -1
  51. data/lib/generator_templates/controlplane.yml +5 -0
  52. data/lib/generator_templates/templates/{gvc.yml → app.yml} +4 -4
  53. data/lib/generator_templates/templates/postgres.yml +1 -1
  54. data/lib/generator_templates/templates/rails.yml +1 -1
  55. data/script/check_cpln_links +3 -3
  56. data/templates/app.yml +18 -0
  57. data/templates/daily-task.yml +3 -2
  58. data/templates/rails.yml +3 -2
  59. data/templates/secrets.yml +11 -0
  60. data/templates/sidekiq.yml +3 -2
  61. metadata +38 -25
  62. data/.rspec +0 -1
  63. data/lib/command/run_cleanup.rb +0 -116
  64. data/lib/command/run_detached.rb +0 -175
  65. data/lib/core/scripts.rb +0 -34
  66. data/templates/gvc.yml +0 -13
  67. 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 **_interactive_** replicas (analog of `heroku run`)"
31
+ DESCRIPTION = "Runs one-off interactive or non-interactive replicas (analog of `heroku run`)"
18
32
  LONG_DESCRIPTION = <<~DESC
19
- - Runs one-off **_interactive_** replicas (analog of `heroku run`)
20
- - Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
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`)
22
-
23
- > **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
24
- > task crashing are tolerable. For production tasks, it's better to use `cpl run:detached`.
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
- # Need to quote COMMAND if setting ENV value or passing args.
32
- cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
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, displays output, and exits shell.
35
- cpl run -a $APP_NAME -- ls /
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 keeps shell open.
39
- cpl run -a $APP_NAME -- rails c
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
- attr_reader :location, :workload_to_clone, :workload_clone, :container
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
- @workload_to_clone = config.options["workload"] || config[:one_off_workload]
59
- @workload_clone = "#{workload_to_clone}-run-#{random_four_digits}"
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
- step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
62
- clone_workload
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
- ensure_workload_deleted(workload_clone)
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 clone_workload # rubocop:disable Metrics/MethodLength
76
- # Create a base copy of workload props
77
- spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
78
- container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
79
- @container = container_spec["name"]
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
- # remove other containers if any
82
- spec["containers"] = [container_spec]
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
- # Stub workload command with dummy server that just responds to port
85
- # Needed to avoid execution of ENTRYPOINT and CMD of Dockerfile
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
- # Ensure one-off workload will be running
90
- spec["defaultOptions"]["suspend"] = false
153
+ # Default to using existing Dockerfile entrypoint
154
+ container_spec.delete("command")
91
155
 
92
- # Ensure no scaling
93
- spec["defaultOptions"]["autoscaling"]["minScale"] = 1
94
- spec["defaultOptions"]["autoscaling"]["maxScale"] = 1
95
- spec["defaultOptions"]["capacityAI"] = false
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
- # Override image if specified
98
- image = config.options[:image]
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
- # Set runner
103
- container_spec["env"] ||= []
104
- container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
165
+ # Ensure no scaling
166
+ spec["defaultOptions"]["autoscaling"] = {}
167
+ spec["defaultOptions"]["capacityAI"] = false
105
168
 
106
- if config.options["use_local_token"]
107
- container_spec["env"] << { "name" => "CONTROLPLANE_TOKEN", "value" => ControlplaneApiDirect.new.api_token }
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
- # Create workload clone
111
- cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
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 runner_script # rubocop:disable Metrics/MethodLength
115
- script = Scripts.helpers_cleanup
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
- if config.options["use_local_token"]
118
- script += <<~SHELL
119
- CPLN_TOKEN=$CONTROLPLANE_TOKEN
120
- unset CONTROLPLANE_TOKEN
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" if rows && cols
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 run_in_replica
139
- progress.puts("Connecting...\n\n")
140
- command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
141
- cp.workload_exec(workload_clone, location: location, container: container, command: command)
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
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExitCode
4
+ SUCCESS = 0
5
+ ERROR_DEFAULT = 64
6
+ INTERRUPT = 130
7
+ 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
- @app_configs[app_name1] ||= apps.find do |app_name2, app_config|
115
- app_matches?(app_name1, app_name2, app_config)
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