cpl 0.1.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 +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
|