Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate spec/dummy app #134

Merged
merged 5 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ _Please add entries here for your pull requests that are not yet released._

### Added

- Added `--no-clean-on-failure` option to `run:detached` command to skip deletion of failed workload run. [PR 133](https://github.com/shakacode/heroku-to-control-plane/pull/133) by [Justin Gordon](https://github.com/justin808) and [Rafael Gomes](https://github.com/rafaelgomesxyz).
- Added `--domain` option to `maintenance`, `maintenance:on` and `maintenance:off` commands. [PR 131](https://github.com/shakacode/heroku-to-control-plane/pull/131) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
- Added `default_domain` config to specify domain for `maintenance`, `maintenance:on` and `maintenance:off` commands. [PR 131](https://github.com/shakacode/heroku-to-control-plane/pull/131) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
- Added option to specify upstream for `copy-image-from-upstream` command through `CPLN_UPSTREAM` env var. [PR 138](https://github.com/shakacode/heroku-to-control-plane/pull/138) by [Rafael Gomes](https://github.com/rafaelgomesxyz).
Expand Down
31 changes: 14 additions & 17 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,28 +332,25 @@ cpl ps:swait -a $APP_NAME -w $WORKLOAD_NAME
cpl run -a $APP_NAME

# Need to quote COMMAND if setting ENV value or passing args.
cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME

# COMMAND may also be passed at the end.
cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'

# Runs command, displays output, and exits shell.
cpl run ls / -a $APP_NAME
cpl run rails db:migrate:status -a $APP_NAME
cpl run -a $APP_NAME -- ls /
cpl run -a $APP_NAME -- rails db:migrate:status

# Runs command and keeps shell open.
cpl run rails c -a $APP_NAME
cpl run -a $APP_NAME -- rails c

# Uses a different image (which may not be promoted yet).
cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
cpl run -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
cpl run -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image

# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
cpl run bash -a $APP_NAME -w other-workload
cpl run -a $APP_NAME -w other-workload -- bash

# Overrides remote CPLN_TOKEN env variable with local token.
# Useful when superuser rights are needed in remote container.
cpl run bash -a $APP_NAME --use-local-token
cpl run -a $APP_NAME --use-local-token -- bash
```

### `run:cleanup`
Expand All @@ -375,26 +372,26 @@ cpl run:cleanup -a $APP_NAME
- Implemented with only async execution methods, more suitable for production tasks
- Has alternative log fetch implementation with only JSON-polling and no WebSockets
- Less responsive but more stable, useful for CI tasks
- Deletes the workload whenever finished with success
- Deletes the workload whenever finished with failure by default
- Use `--no-clean-on-failure` to disable cleanup to help with debugging failed runs

```sh
cpl run:detached rails db:prepare -a $APP_NAME

# Need to quote COMMAND if setting ENV value or passing args.
cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME

# COMMAND may also be passed at the end.
cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'

# Uses a different image (which may not be promoted yet).
cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
cpl run:detached -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
cpl run:detached -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image

# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
cpl run:detached rails db:migrate:status -a $APP_NAME -w other-workload
cpl run:detached -a $APP_NAME -w other-workload -- rails db:migrate:status

# Overrides remote CPLN_TOKEN env variable with local token.
# Useful when superuser rights are needed in remote container.
cpl run:detached rails db:migrate:status -a $APP_NAME --use-local-token
cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
```

### `setup-app`
Expand Down
16 changes: 16 additions & 0 deletions lib/command/base.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# frozen_string_literal: true

require_relative "../core/helpers"

module Command
class Base # rubocop:disable Metrics/ClassLength
attr_reader :config

include Helpers

# Used to call the command (`cpl NAME`)
# NAME = ""
# Displayed when running `cpl help` or `cpl help NAME` (defaults to `NAME`)
Expand Down Expand Up @@ -233,6 +237,18 @@ def self.trace_option(required: false)
}
end

def self.clean_on_failure_option(required: false)
{
name: :clean_on_failure,
params: {
desc: "Deletes workload when finished with failure (success always deletes)",
type: :boolean,
required: required,
default: true
}
}
end

def self.all_options
methods.grep(/_option$/).map { |method| send(method.to_s) }
end
Expand Down
2 changes: 1 addition & 1 deletion lib/command/copy_image_from_upstream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def ensure_upstream_org!
def create_upstream_profile
step("Creating upstream profile") do
loop do
@upstream_profile = "upstream-#{rand(1000..9999)}"
@upstream_profile = "upstream-#{random_four_digits}"
break unless cp.profile_exists?(@upstream_profile)
end

Expand Down
39 changes: 18 additions & 21 deletions lib/command/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,56 +29,53 @@ class Run < Base
cpl run -a $APP_NAME

# Need to quote COMMAND if setting ENV value or passing args.
cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME

# COMMAND may also be passed at the end.
cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'

# Runs command, displays output, and exits shell.
cpl run ls / -a $APP_NAME
cpl run rails db:migrate:status -a $APP_NAME
cpl run -a $APP_NAME -- ls /
cpl run -a $APP_NAME -- rails db:migrate:status

# Runs command and keeps shell open.
cpl run rails c -a $APP_NAME
cpl run -a $APP_NAME -- rails c

# Uses a different image (which may not be promoted yet).
cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
cpl run -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
cpl run -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image

# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
cpl run bash -a $APP_NAME -w other-workload
cpl run -a $APP_NAME -w other-workload -- bash

# Overrides remote CPLN_TOKEN env variable with local token.
# Useful when superuser rights are needed in remote container.
cpl run bash -a $APP_NAME --use-local-token
cpl run -a $APP_NAME --use-local-token -- bash
```
EX

attr_reader :location, :workload, :one_off, :container
attr_reader :location, :workload_to_clone, :workload_clone, :container

def call # rubocop:disable Metrics/MethodLength
@location = config.location
@workload = config.options["workload"] || config[:one_off_workload]
@one_off = "#{workload}-run-#{rand(1000..9999)}"
@workload_to_clone = config.options["workload"] || config[:one_off_workload]
@workload_clone = "#{workload_to_clone}-run-#{random_four_digits}"

step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
clone_workload
end

wait_for_workload(one_off)
wait_for_replica(one_off, location)
wait_for_workload(workload_clone)
wait_for_replica(workload_clone, location)
run_in_replica
ensure
progress.puts
ensure_workload_deleted(one_off)
ensure_workload_deleted(workload_clone)
end

private

def clone_workload # rubocop:disable Metrics/MethodLength
# Create a base copy of workload props
spec = cp.fetch_workload!(workload).fetch("spec")
container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
@container = container_spec["name"]

# remove other containers if any
Expand Down Expand Up @@ -111,7 +108,7 @@ def clone_workload # rubocop:disable Metrics/MethodLength
end

# Create workload clone
cp.apply_hash("kind" => "workload", "name" => one_off, "spec" => spec)
cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
end

def runner_script # rubocop:disable Metrics/MethodLength
Expand Down Expand Up @@ -141,7 +138,7 @@ def runner_script # rubocop:disable Metrics/MethodLength
def run_in_replica
progress.puts("Connecting...\n\n")
command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
cp.workload_exec(one_off, location: location, container: container, command: command)
cp.workload_exec(workload_clone, location: location, container: container, command: command)
end
end
end
65 changes: 36 additions & 29 deletions lib/command/run_detached.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ class RunDetached < Base # rubocop:disable Metrics/ClassLength
image_option,
workload_option,
location_option,
use_local_token_option
use_local_token_option,
clean_on_failure_option
].freeze
DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
LONG_DESCRIPTION = <<~DESC
Expand All @@ -19,59 +20,55 @@ class RunDetached < Base # rubocop:disable Metrics/ClassLength
- Implemented with only async execution methods, more suitable for production tasks
- Has alternative log fetch implementation with only JSON-polling and no WebSockets
- Less responsive but more stable, useful for CI tasks
- Deletes the workload whenever finished with success
- Deletes the workload whenever finished with failure by default
- Use `--no-clean-on-failure` to disable cleanup to help with debugging failed runs
DESC
EXAMPLES = <<~EX
```sh
cpl run:detached rails db:prepare -a $APP_NAME

# Need to quote COMMAND if setting ENV value or passing args.
cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME

# COMMAND may also be passed at the end.
cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'

# Uses a different image (which may not be promoted yet).
cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
cpl run:detached -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
cpl run:detached -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image

# Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
cpl run:detached rails db:migrate:status -a $APP_NAME -w other-workload
cpl run:detached -a $APP_NAME -w other-workload -- rails db:migrate:status

# Overrides remote CPLN_TOKEN env variable with local token.
# Useful when superuser rights are needed in remote container.
cpl run:detached rails db:migrate:status -a $APP_NAME --use-local-token
cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
```
EX

WORKLOAD_SLEEP_CHECK = 2

attr_reader :location, :workload, :one_off, :container
attr_reader :location, :workload_to_clone, :workload_clone, :container

def call # rubocop:disable Metrics/MethodLength
def call
@location = config.location
@workload = config.options["workload"] || config[:one_off_workload]
@one_off = "#{workload}-runner-#{rand(1000..9999)}"
@workload_to_clone = config.options["workload"] || config[:one_off_workload]
@workload_clone = "#{workload_to_clone}-runner-#{random_four_digits}"

step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
clone_workload
end

wait_for_workload(one_off)
wait_for_workload(workload_clone)
show_logs_waiting
ensure
if cp.fetch_workload(one_off)
progress.puts
ensure_workload_deleted(one_off)
end
exit(1) if @crashed
end

private

def clone_workload # rubocop:disable Metrics/MethodLength
# Get base specs of workload
spec = cp.fetch_workload!(workload).fetch("spec")
container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
@container = container_spec["name"]

# remove other containers if any
Expand Down Expand Up @@ -105,7 +102,7 @@ def clone_workload # rubocop:disable Metrics/MethodLength
container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }

# Create workload clone
cp.apply_hash("kind" => "workload", "name" => one_off, "spec" => spec)
cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
end

def runner_script # rubocop:disable Metrics/MethodLength
Expand All @@ -119,12 +116,20 @@ def runner_script # rubocop:disable Metrics/MethodLength
end

script += <<~SHELL
if ! eval "#{args_join(config.args)}"; then echo "----- CRASHED -----"; fi

echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
sleep 10 # grace time for logs propagation
curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
while true; do sleep 1; done # wait for SIGTERM
crashed=0
if ! eval "#{args_join(config.args)}"; then
crashed=1
echo "----- CRASHED -----"
fi
clean_on_failure=#{config.options[:clean_on_failure] ? 1 : 0}
if [ $crashed -eq 0 ] || [ $clean_on_failure -eq 1 ]; then
echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
sleep 30 # grace time for logs propagation
curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
while true; do sleep 1; done # wait for SIGTERM
else
echo "-- FINISHED RUNNER SCRIPT --"
fi
SHELL

script
Expand All @@ -133,7 +138,8 @@ def runner_script # rubocop:disable Metrics/MethodLength
def show_logs_waiting # rubocop:disable Metrics/MethodLength
progress.puts("Scheduled, fetching logs (it's a cron job, so it may take up to a minute to start)...\n\n")
begin
while cp.fetch_workload(one_off)
@finished = false
while cp.fetch_workload(workload_clone) && !@finished
sleep(WORKLOAD_SLEEP_CHECK)
print_uniq_logs
end
Expand All @@ -151,14 +157,15 @@ def print_uniq_logs

(entries - @printed_log_entries).sort.each do |(_ts, val)|
@crashed = true if val.match?(/^----- CRASHED -----$/)
@finished = true if val.match?(/^-- FINISHED RUNNER SCRIPT(, DELETING WORKLOAD)? --$/)
puts val
end

@printed_log_entries = entries # as well truncate old entries if any
end

def normalized_log_entries(from:, to:)
log = cp.log_get(workload: one_off, from: from, to: to)
log = cp.log_get(workload: workload_clone, from: from, to: to)

log["data"]["result"]
.each_with_object([]) { |obj, result| result.concat(obj["values"]) }
Expand Down
6 changes: 6 additions & 0 deletions lib/core/helpers.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# frozen_string_literal: true

require "securerandom"

module Helpers
def strip_str_and_validate(str)
return str if str.nil?

str = str.strip
str.empty? ? nil : str
end

def random_four_digits
SecureRandom.random_number(1000..9999)
end
end
4 changes: 3 additions & 1 deletion spec/cpl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
args = [option_key_name, option_value]
end

allow(Config).to receive(:new).with([], { option[:name].to_sym => option_value }, []).and_call_original
allow(Config).to receive(:new)
.with([], hash_including(option[:name].to_sym => option_value), [])
.and_call_original

allow_any_instance_of(Config).to receive(:config_file_path).and_return("spec/fixtures/config.yml") # rubocop:disable RSpec/AnyInstance
expect_any_instance_of(Command::Test).to receive(:call) # rubocop:disable RSpec/AnyInstance
Expand Down
Loading
Loading