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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +60 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +16 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CONTRIBUTING.md +12 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +104 -0
  10. data/LICENSE +21 -0
  11. data/README.md +318 -0
  12. data/Rakefile +11 -0
  13. data/bin/cpl +6 -0
  14. data/cpl +15 -0
  15. data/cpl.gemspec +42 -0
  16. data/docs/commands.md +219 -0
  17. data/docs/troubleshooting.md +6 -0
  18. data/examples/circleci.yml +106 -0
  19. data/examples/controlplane.yml +44 -0
  20. data/lib/command/base.rb +177 -0
  21. data/lib/command/build_image.rb +25 -0
  22. data/lib/command/config.rb +33 -0
  23. data/lib/command/delete.rb +50 -0
  24. data/lib/command/env.rb +21 -0
  25. data/lib/command/exists.rb +23 -0
  26. data/lib/command/latest_image.rb +18 -0
  27. data/lib/command/logs.rb +29 -0
  28. data/lib/command/open.rb +33 -0
  29. data/lib/command/promote_image.rb +27 -0
  30. data/lib/command/ps.rb +40 -0
  31. data/lib/command/ps_restart.rb +34 -0
  32. data/lib/command/ps_start.rb +34 -0
  33. data/lib/command/ps_stop.rb +34 -0
  34. data/lib/command/run.rb +106 -0
  35. data/lib/command/run_detached.rb +148 -0
  36. data/lib/command/setup.rb +59 -0
  37. data/lib/command/test.rb +26 -0
  38. data/lib/core/config.rb +81 -0
  39. data/lib/core/controlplane.rb +128 -0
  40. data/lib/core/controlplane_api.rb +51 -0
  41. data/lib/core/controlplane_api_cli.rb +10 -0
  42. data/lib/core/controlplane_api_direct.rb +42 -0
  43. data/lib/core/scripts.rb +34 -0
  44. data/lib/cpl/version.rb +5 -0
  45. data/lib/cpl.rb +139 -0
  46. data/lib/main.rb +5 -0
  47. data/postgres.md +436 -0
  48. data/redis.md +112 -0
  49. data/script/generate_commands_docs +60 -0
  50. data/templates/gvc.yml +13 -0
  51. data/templates/identity.yml +2 -0
  52. data/templates/memcached.yml +23 -0
  53. data/templates/postgres.yml +31 -0
  54. data/templates/rails.yml +25 -0
  55. data/templates/redis.yml +20 -0
  56. data/templates/sidekiq.yml +28 -0
  57. 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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