cpl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +60 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +12 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +104 -0
- data/LICENSE +21 -0
- data/README.md +318 -0
- data/Rakefile +11 -0
- data/bin/cpl +6 -0
- data/cpl +15 -0
- data/cpl.gemspec +42 -0
- data/docs/commands.md +219 -0
- data/docs/troubleshooting.md +6 -0
- data/examples/circleci.yml +106 -0
- data/examples/controlplane.yml +44 -0
- data/lib/command/base.rb +177 -0
- data/lib/command/build_image.rb +25 -0
- data/lib/command/config.rb +33 -0
- data/lib/command/delete.rb +50 -0
- data/lib/command/env.rb +21 -0
- data/lib/command/exists.rb +23 -0
- data/lib/command/latest_image.rb +18 -0
- data/lib/command/logs.rb +29 -0
- data/lib/command/open.rb +33 -0
- data/lib/command/promote_image.rb +27 -0
- data/lib/command/ps.rb +40 -0
- data/lib/command/ps_restart.rb +34 -0
- data/lib/command/ps_start.rb +34 -0
- data/lib/command/ps_stop.rb +34 -0
- data/lib/command/run.rb +106 -0
- data/lib/command/run_detached.rb +148 -0
- data/lib/command/setup.rb +59 -0
- data/lib/command/test.rb +26 -0
- data/lib/core/config.rb +81 -0
- data/lib/core/controlplane.rb +128 -0
- data/lib/core/controlplane_api.rb +51 -0
- data/lib/core/controlplane_api_cli.rb +10 -0
- data/lib/core/controlplane_api_direct.rb +42 -0
- data/lib/core/scripts.rb +34 -0
- data/lib/cpl/version.rb +5 -0
- data/lib/cpl.rb +139 -0
- data/lib/main.rb +5 -0
- data/postgres.md +436 -0
- data/redis.md +112 -0
- data/script/generate_commands_docs +60 -0
- data/templates/gvc.yml +13 -0
- data/templates/identity.yml +2 -0
- data/templates/memcached.yml +23 -0
- data/templates/postgres.yml +31 -0
- data/templates/rails.yml +25 -0
- data/templates/redis.yml +20 -0
- data/templates/sidekiq.yml +28 -0
- metadata +312 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class LatestImage < Base
|
5
|
+
NAME = "latest-image"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Displays the latest image name"
|
10
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
11
|
+
- Displays the latest image name
|
12
|
+
HEREDOC
|
13
|
+
|
14
|
+
def call
|
15
|
+
puts latest_image
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/command/logs.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Logs < Base
|
5
|
+
NAME = "logs"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Light wrapper to display tailed raw logs for app/workload syntax"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Light wrapper to display tailed raw logs for app/workload syntax
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Displays logs for the default workload (`one_off_workload`).
|
17
|
+
cpl logs -a $APP_NAME
|
18
|
+
|
19
|
+
# Displays logs for a specific workload.
|
20
|
+
cpl logs -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call
|
25
|
+
workload = config.options[:workload] || config[:one_off_workload]
|
26
|
+
cp.logs(workload: workload)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/command/open.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Open < Base
|
5
|
+
NAME = "open"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Opens the app endpoint URL in the default browser"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Opens the app endpoint URL in the default browser
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Opens the endpoint of the default workload (`one_off_workload`).
|
17
|
+
cpl open -a $APP_NAME
|
18
|
+
|
19
|
+
# Opens the endpoint of a specific workload.
|
20
|
+
cpl open -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call
|
25
|
+
workload = config.options[:workload] || config[:one_off_workload]
|
26
|
+
data = cp.workload_get(workload)
|
27
|
+
url = data["status"]["endpoint"]
|
28
|
+
opener = `which xdg-open open`.split("\n").grep_v("not found").first
|
29
|
+
|
30
|
+
exec %(#{opener} "#{url}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class PromoteImage < Base
|
5
|
+
NAME = "promote-image"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true)
|
8
|
+
].freeze
|
9
|
+
DESCRIPTION = "Promotes the latest image to app workloads"
|
10
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
11
|
+
- Promotes the latest image to app workloads
|
12
|
+
HEREDOC
|
13
|
+
|
14
|
+
def call
|
15
|
+
image = latest_image
|
16
|
+
|
17
|
+
config[:app_workloads].each do |workload|
|
18
|
+
cp.workload_get(workload).dig("spec", "containers").each do |container|
|
19
|
+
next unless container["image"].match?(%r{^/org/#{config[:cpln_org]}/image/#{config.app}:})
|
20
|
+
|
21
|
+
cp.workload_set_image_ref(workload, container: container["name"], image: image)
|
22
|
+
progress.puts "updated #{container['name']}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/command/ps.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Ps < Base
|
5
|
+
NAME = "ps"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Shows running replicas in app"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Shows running replicas in app
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Shows running replicas in app, for all workloads.
|
17
|
+
cpl ps -a $APP_NAME
|
18
|
+
|
19
|
+
# Shows running replicas in app, for a specific workload.
|
20
|
+
cpl ps -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call # rubocop:disable Metrics/MethodLength
|
25
|
+
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
+
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
|
+
|
28
|
+
workloads.each do |workload|
|
29
|
+
result = cp.workload_get_replicas(workload, location: config[:default_location])
|
30
|
+
if result.nil?
|
31
|
+
puts "#{workload}: no workload"
|
32
|
+
elsif result["items"].nil?
|
33
|
+
puts "#{workload}: no replicas"
|
34
|
+
else
|
35
|
+
result["items"].each { |replica| puts replica }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class PsRestart < Base
|
5
|
+
NAME = "ps:restart"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Forces redeploy of workloads in app"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Forces redeploy of workloads in app
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Forces redeploy of all workloads in app.
|
17
|
+
cpl ps:restart -a $APP_NAME
|
18
|
+
|
19
|
+
# Forces redeploy of a specific workload in app.
|
20
|
+
cpl ps:restart -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call
|
25
|
+
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
+
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
|
+
|
28
|
+
workloads.each do |workload|
|
29
|
+
cp.workload_force_redeployment(workload)
|
30
|
+
progress.puts "#{workload} restarted"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class PsStart < Base
|
5
|
+
NAME = "ps:start"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Starts workloads in app"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Starts workloads in app
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Starts all workloads in app.
|
17
|
+
cpl ps:start -a $APP_NAME
|
18
|
+
|
19
|
+
# Starts a specific workload in app.
|
20
|
+
cpl ps:start -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call
|
25
|
+
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
+
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
|
+
|
28
|
+
workloads.reverse_each do |workload|
|
29
|
+
cp.workload_set_suspend(workload, false)
|
30
|
+
progress.puts "#{workload} started"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class PsStop < Base
|
5
|
+
NAME = "ps:stop"
|
6
|
+
OPTIONS = [
|
7
|
+
app_option(required: true),
|
8
|
+
workload_option
|
9
|
+
].freeze
|
10
|
+
DESCRIPTION = "Stops workloads in app"
|
11
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
12
|
+
- Stops workloads in app
|
13
|
+
HEREDOC
|
14
|
+
EXAMPLES = <<~HEREDOC
|
15
|
+
```sh
|
16
|
+
# Stops all workloads in app.
|
17
|
+
cpl ps:stop -a $APP_NAME
|
18
|
+
|
19
|
+
# Stops a specific workload in app.
|
20
|
+
cpl ps:stop -a $APP_NAME -w $WORKLOAD_NAME
|
21
|
+
```
|
22
|
+
HEREDOC
|
23
|
+
|
24
|
+
def call
|
25
|
+
workloads = [config.options[:workload]] if config.options[:workload]
|
26
|
+
workloads ||= config[:app_workloads] + config[:additional_workloads]
|
27
|
+
|
28
|
+
workloads.each do |workload|
|
29
|
+
cp.workload_set_suspend(workload, true)
|
30
|
+
progress.puts "#{workload} stopped"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/command/run.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Run < Base
|
5
|
+
NAME = "run"
|
6
|
+
USAGE = "run COMMAND"
|
7
|
+
REQUIRES_ARGS = true
|
8
|
+
DEFAULT_ARGS = ["bash"].freeze
|
9
|
+
OPTIONS = [
|
10
|
+
app_option(required: true),
|
11
|
+
image_option
|
12
|
+
].freeze
|
13
|
+
DESCRIPTION = "Runs one-off **_interactive_** replicas (analog of `heroku run`)"
|
14
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
15
|
+
- Runs one-off **_interactive_** replicas (analog of `heroku run`)
|
16
|
+
- Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
|
17
|
+
- May not work correctly with tasks that last over 5 minutes (there's a Control Plane scaling bug at the moment)
|
18
|
+
|
19
|
+
> **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
|
20
|
+
> task crashing are tolerable. For production tasks, it's better to use `cpl run:detached`.
|
21
|
+
HEREDOC
|
22
|
+
EXAMPLES = <<~HEREDOC
|
23
|
+
```sh
|
24
|
+
# Opens shell (bash by default).
|
25
|
+
cpl run -a $APP_NAME
|
26
|
+
|
27
|
+
# Runs command, displays output, and exits shell.
|
28
|
+
cpl run ls / -a $APP_NAME
|
29
|
+
cpl run rails db:migrate:status -a $APP_NAME
|
30
|
+
|
31
|
+
# Runs command and keeps shell open.
|
32
|
+
cpl run rails c -a $APP_NAME
|
33
|
+
|
34
|
+
# Uses a different image (which may not be promoted yet).
|
35
|
+
cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
|
36
|
+
cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
|
37
|
+
```
|
38
|
+
HEREDOC
|
39
|
+
|
40
|
+
attr_reader :location, :workload, :one_off
|
41
|
+
|
42
|
+
def call
|
43
|
+
@location = config[:default_location]
|
44
|
+
@workload = config[:one_off_workload]
|
45
|
+
@one_off = "#{workload}-run-#{rand(1000..9999)}"
|
46
|
+
|
47
|
+
clone_workload
|
48
|
+
wait_for_workload(one_off)
|
49
|
+
sleep 2 # sometimes replica query lags workload creation, despite ok by prev query
|
50
|
+
wait_for_replica(one_off, location)
|
51
|
+
run_in_replica
|
52
|
+
ensure
|
53
|
+
ensure_workload_deleted(one_off)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def clone_workload # rubocop:disable Metrics/MethodLength
|
59
|
+
progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
|
60
|
+
|
61
|
+
# Create a base copy of workload props
|
62
|
+
spec = cp.workload_get(workload).fetch("spec")
|
63
|
+
container = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
|
64
|
+
|
65
|
+
# remove other containers if any
|
66
|
+
spec["containers"] = [container]
|
67
|
+
|
68
|
+
# Stub workload command with dummy server that just responds to port
|
69
|
+
# Needed to avoid execution of ENTRYPOINT and CMD of Dockerfile
|
70
|
+
container["command"] = "ruby"
|
71
|
+
container["args"] = ["-e", Scripts.http_dummy_server_ruby]
|
72
|
+
|
73
|
+
# Ensure one-off workload will be running
|
74
|
+
spec["defaultOptions"]["suspend"] = false
|
75
|
+
|
76
|
+
# Ensure no scaling
|
77
|
+
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
78
|
+
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
79
|
+
spec["defaultOptions"]["capacityAI"] = false
|
80
|
+
|
81
|
+
# Override image if specified
|
82
|
+
image = config.options[:image]
|
83
|
+
image = "/org/#{config[:cpln_org]}/image/#{latest_image}" if image == "latest"
|
84
|
+
container["image"] = image if image
|
85
|
+
|
86
|
+
# Set runner
|
87
|
+
container["env"] ||= []
|
88
|
+
container["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
|
89
|
+
|
90
|
+
# Create workload clone
|
91
|
+
cp.apply("kind" => "workload", "name" => one_off, "spec" => spec)
|
92
|
+
end
|
93
|
+
|
94
|
+
def runner_script
|
95
|
+
script = Scripts.helpers_cleanup
|
96
|
+
script += args_join(config.args)
|
97
|
+
script
|
98
|
+
end
|
99
|
+
|
100
|
+
def run_in_replica
|
101
|
+
progress.puts "- Connecting"
|
102
|
+
command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
|
103
|
+
cp.workload_exec(one_off, location: location, container: workload, command: command)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class RunDetached < Base # rubocop:disable Metrics/ClassLength
|
5
|
+
NAME = "run:detached"
|
6
|
+
USAGE = "run:detached COMMAND"
|
7
|
+
REQUIRES_ARGS = true
|
8
|
+
OPTIONS = [
|
9
|
+
app_option(required: true),
|
10
|
+
image_option
|
11
|
+
].freeze
|
12
|
+
DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
|
13
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
14
|
+
- Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)
|
15
|
+
- Uses `Cron` workload type with log async fetching
|
16
|
+
- Implemented with only async execution methods, more suitable for production tasks
|
17
|
+
- Has alternative log fetch implementation with only JSON-polling and no WebSockets
|
18
|
+
- Less responsive but more stable, useful for CI tasks
|
19
|
+
HEREDOC
|
20
|
+
EXAMPLES = <<~HEREDOC
|
21
|
+
```sh
|
22
|
+
cpl run:detached rails db:prepare -a $APP_NAME
|
23
|
+
cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
|
24
|
+
|
25
|
+
# Uses some other image.
|
26
|
+
cpl run:detached rails db:migrate -a $APP_NAME --image /some/full/image/path
|
27
|
+
|
28
|
+
# Uses latest app image (which may not be promoted yet).
|
29
|
+
cpl run:detached rails db:migrate -a $APP_NAME --image latest
|
30
|
+
|
31
|
+
# Uses a different image (which may not be promoted yet).
|
32
|
+
cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
|
33
|
+
cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
|
34
|
+
```
|
35
|
+
HEREDOC
|
36
|
+
|
37
|
+
WORKLOAD_SLEEP_CHECK = 2
|
38
|
+
|
39
|
+
attr_reader :location, :workload, :one_off
|
40
|
+
|
41
|
+
def call
|
42
|
+
@location = config[:default_location]
|
43
|
+
@workload = config[:one_off_workload]
|
44
|
+
@one_off = "#{workload}-runner-#{rand(1000..9999)}"
|
45
|
+
|
46
|
+
clone_workload
|
47
|
+
wait_for_workload(one_off)
|
48
|
+
show_logs_waiting
|
49
|
+
ensure
|
50
|
+
ensure_workload_deleted(one_off)
|
51
|
+
exit(1) if @crashed
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def clone_workload # rubocop:disable Metrics/MethodLength
|
57
|
+
progress.puts "- Cloning workload '#{workload}' on '#{config.options[:app]}' to '#{one_off}'"
|
58
|
+
|
59
|
+
# Get base specs of workload
|
60
|
+
spec = cp.workload_get(workload).fetch("spec")
|
61
|
+
container = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
|
62
|
+
|
63
|
+
# remove other containers if any
|
64
|
+
spec["containers"] = [container]
|
65
|
+
|
66
|
+
# Set runner
|
67
|
+
container["command"] = "bash"
|
68
|
+
container["args"] = ["-c", 'eval "$CONTROLPLANE_RUNNER"']
|
69
|
+
|
70
|
+
# Ensure one-off workload will be running
|
71
|
+
spec["defaultOptions"]["suspend"] = false
|
72
|
+
|
73
|
+
# Ensure no scaling
|
74
|
+
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
75
|
+
spec["defaultOptions"]["autoscaling"]["minScale"] = 1
|
76
|
+
spec["defaultOptions"]["capacityAI"] = false
|
77
|
+
|
78
|
+
# Override image if specified
|
79
|
+
image = config.options[:image]
|
80
|
+
image = "/org/#{config[:cpln_org]}/image/#{latest_image}" if image == "latest"
|
81
|
+
container["image"] = image if image
|
82
|
+
|
83
|
+
# Set cron job props
|
84
|
+
spec["type"] = "cron"
|
85
|
+
spec["job"] = { "schedule" => "* * * * *", "restartPolicy" => "Never" }
|
86
|
+
spec["defaultOptions"]["autoscaling"] = {}
|
87
|
+
container.delete("ports")
|
88
|
+
|
89
|
+
container["env"] ||= []
|
90
|
+
container["env"] << { "name" => "CONTROLPLANE_TOKEN", "value" => ControlplaneApiDirect.new.api_token }
|
91
|
+
container["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
|
92
|
+
|
93
|
+
# Create workload clone
|
94
|
+
cp.apply("kind" => "workload", "name" => one_off, "spec" => spec)
|
95
|
+
end
|
96
|
+
|
97
|
+
def runner_script
|
98
|
+
script = "echo '-- STARTED RUNNER SCRIPT --'\n"
|
99
|
+
script += Scripts.helpers_cleanup
|
100
|
+
|
101
|
+
script += <<~SHELL
|
102
|
+
if ! eval "#{args_join(config.args)}"; then echo "----- CRASHED -----"; fi
|
103
|
+
|
104
|
+
echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
|
105
|
+
sleep 10 # grace time for logs propagation
|
106
|
+
curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
|
107
|
+
while true; do sleep 1; done # wait for SIGTERM
|
108
|
+
SHELL
|
109
|
+
|
110
|
+
script
|
111
|
+
end
|
112
|
+
|
113
|
+
def show_logs_waiting # rubocop:disable Metrics/MethodLength
|
114
|
+
progress.puts "- Scheduled, fetching logs (it is cron job, so it may take up to a minute to start)"
|
115
|
+
begin
|
116
|
+
while cp.workload_get(one_off)
|
117
|
+
sleep(WORKLOAD_SLEEP_CHECK)
|
118
|
+
print_uniq_logs
|
119
|
+
end
|
120
|
+
rescue RuntimeError => e
|
121
|
+
progress.puts "ERROR: #{e}"
|
122
|
+
retry
|
123
|
+
end
|
124
|
+
progress.puts "- Finished workload and logger"
|
125
|
+
end
|
126
|
+
|
127
|
+
def print_uniq_logs
|
128
|
+
@printed_log_entries ||= []
|
129
|
+
ts = Time.now.to_i
|
130
|
+
entries = normalized_log_entries(from: ts - 60, to: ts)
|
131
|
+
|
132
|
+
(entries - @printed_log_entries).sort.each do |(_ts, val)|
|
133
|
+
@crashed = true if val.match?(/^----- CRASHED -----$/)
|
134
|
+
puts val
|
135
|
+
end
|
136
|
+
|
137
|
+
@printed_log_entries = entries # as well truncate old entries if any
|
138
|
+
end
|
139
|
+
|
140
|
+
def normalized_log_entries(from:, to:)
|
141
|
+
log = cp.log_get(workload: one_off, from: from, to: to)
|
142
|
+
|
143
|
+
log["data"]["result"]
|
144
|
+
.each_with_object([]) { |obj, result| result.concat(obj["values"]) }
|
145
|
+
.select { |ts, _val| ts[..-10].to_i > from }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Setup < Base
|
5
|
+
NAME = "setup"
|
6
|
+
USAGE = "setup TEMPLATE [TEMPLATE] ... [TEMPLATE]"
|
7
|
+
REQUIRES_ARGS = true
|
8
|
+
OPTIONS = [
|
9
|
+
app_option(required: true)
|
10
|
+
].freeze
|
11
|
+
DESCRIPTION = "Applies application-specific configs from templates"
|
12
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
13
|
+
- Applies application-specific configs from templates (e.g., for every review-app)
|
14
|
+
- Publishes (creates or updates) those at Control Plane infrastructure
|
15
|
+
- Picks templates from the `.controlplane/templates` directory
|
16
|
+
- Templates are ordinary Control Plane templates but with variable preprocessing
|
17
|
+
|
18
|
+
**Preprocessed template variables:**
|
19
|
+
|
20
|
+
```
|
21
|
+
APP_GVC - basically GVC or app name
|
22
|
+
APP_LOCATION - default location
|
23
|
+
APP_ORG - organization
|
24
|
+
APP_IMAGE - will use latest app image
|
25
|
+
```
|
26
|
+
HEREDOC
|
27
|
+
EXAMPLES = <<~HEREDOC
|
28
|
+
```sh
|
29
|
+
# Applies single template.
|
30
|
+
cpl setup redis -a $APP_NAME
|
31
|
+
|
32
|
+
# Applies several templates (practically creating full app).
|
33
|
+
cpl setup gvc postgres redis rails -a $APP_NAME
|
34
|
+
```
|
35
|
+
HEREDOC
|
36
|
+
|
37
|
+
def call
|
38
|
+
config.args.each do |template|
|
39
|
+
filename = "#{config.app_cpln_dir}/templates/#{template}.yml"
|
40
|
+
abort("ERROR: Can't find template for '#{template}' at #{filename}") unless File.exist?(filename)
|
41
|
+
|
42
|
+
apply_template(filename)
|
43
|
+
progress.puts(template)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def apply_template(filename)
|
50
|
+
data = File.read(filename)
|
51
|
+
.gsub("APP_GVC", config.app)
|
52
|
+
.gsub("APP_LOCATION", config[:default_location])
|
53
|
+
.gsub("APP_ORG", config[:cpln_org])
|
54
|
+
.gsub("APP_IMAGE", latest_image)
|
55
|
+
|
56
|
+
cp.apply(YAML.safe_load(data))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/command/test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Be sure to have run: gem install debug
|
4
|
+
require "debug"
|
5
|
+
|
6
|
+
# rubocop:disable Lint/Debugger
|
7
|
+
module Command
|
8
|
+
class Test < Base
|
9
|
+
NAME = "test"
|
10
|
+
OPTIONS = all_options
|
11
|
+
DESCRIPTION = "For debugging purposes"
|
12
|
+
LONG_DESCRIPTION = <<~HEREDOC
|
13
|
+
- For debugging purposes
|
14
|
+
HEREDOC
|
15
|
+
HIDE = true
|
16
|
+
|
17
|
+
def call
|
18
|
+
# Change code here to test.
|
19
|
+
# You can use `debugger` to debug.
|
20
|
+
# debugger
|
21
|
+
# Or print values
|
22
|
+
pp latest_image_next
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# rubocop:enable Lint/Debugger
|
data/lib/core/config.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Config
|
4
|
+
attr_reader :config, :current,
|
5
|
+
:app, :app_dir,
|
6
|
+
# command line options
|
7
|
+
:args, :options
|
8
|
+
|
9
|
+
CONFIG_FILE_LOCATIION = ".controlplane/controlplane.yml"
|
10
|
+
|
11
|
+
def initialize(args, options)
|
12
|
+
@args = args
|
13
|
+
@options = options
|
14
|
+
@app = options[:app]
|
15
|
+
|
16
|
+
load_app_config
|
17
|
+
pick_current_config if app
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](key) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
21
|
+
abort("ERROR: should specify app") unless app
|
22
|
+
|
23
|
+
logger = $stderr
|
24
|
+
|
25
|
+
if current.key?(key)
|
26
|
+
current.fetch(key)
|
27
|
+
elsif key == :cpln_org && current.key?(:org)
|
28
|
+
logger.puts("DEPRECATED: option 'org' is deprecated, use 'cpln_org' instead\n")
|
29
|
+
current.fetch(:org)
|
30
|
+
elsif key == :default_location && current.key?(:location)
|
31
|
+
logger.puts("DEPRECATED: option 'location' is deprecated, use 'default_location' instead\n")
|
32
|
+
current.fetch(:location)
|
33
|
+
elsif key == :match_if_app_name_starts_with && current.key?(:prefix)
|
34
|
+
logger.puts("DEPRECATED: option 'prefix' is deprecated, use 'match_if_app_name_starts_with' instead\n")
|
35
|
+
current.fetch(:prefix)
|
36
|
+
else
|
37
|
+
abort("ERROR: should specify #{key} in controlplane.yml")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def script_path
|
42
|
+
Pathname.new(__dir__).parent.parent
|
43
|
+
end
|
44
|
+
|
45
|
+
def app_cpln_dir
|
46
|
+
"#{app_dir}/.controlplane"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def pick_current_config
|
52
|
+
config[:apps].each do |c_app, c_data|
|
53
|
+
if c_app.to_s == app || (c_data[:match_if_app_name_starts_with] && app.start_with?(c_app.to_s))
|
54
|
+
@current = c_data
|
55
|
+
break
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_app_config
|
61
|
+
config_file = find_app_config_file
|
62
|
+
@config = YAML.safe_load_file(config_file, symbolize_names: true, aliases: true)
|
63
|
+
@app_dir = Pathname.new(config_file).parent.parent.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_app_config_file
|
67
|
+
path = Pathname.new(".").expand_path
|
68
|
+
|
69
|
+
loop do
|
70
|
+
config_file = path + CONFIG_FILE_LOCATIION
|
71
|
+
break config_file if File.file?(config_file)
|
72
|
+
|
73
|
+
path = path.parent
|
74
|
+
|
75
|
+
if path.root?
|
76
|
+
puts "ERROR: Can't find project config file, should be 'project_folder/#{CONFIG_FILE_LOCATIION}'"
|
77
|
+
exit(-1)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|