cpl 1.1.2 → 1.3.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_cpln_links.yml +19 -0
  3. data/.github/workflows/rspec.yml +1 -1
  4. data/.overcommit.yml +3 -0
  5. data/CHANGELOG.md +47 -2
  6. data/CONTRIBUTING.md +2 -6
  7. data/Gemfile.lock +8 -8
  8. data/README.md +57 -15
  9. data/docs/commands.md +29 -23
  10. data/docs/dns.md +9 -0
  11. data/docs/migrating.md +3 -3
  12. data/examples/controlplane.yml +67 -4
  13. data/lib/command/apply_template.rb +2 -1
  14. data/lib/command/base.rb +62 -0
  15. data/lib/command/build_image.rb +5 -1
  16. data/lib/command/config.rb +0 -5
  17. data/lib/command/copy_image_from_upstream.rb +5 -4
  18. data/lib/command/delete.rb +40 -11
  19. data/lib/command/env.rb +1 -0
  20. data/lib/command/generate.rb +45 -0
  21. data/lib/command/info.rb +15 -33
  22. data/lib/command/latest_image.rb +1 -0
  23. data/lib/command/maintenance.rb +9 -4
  24. data/lib/command/maintenance_off.rb +8 -4
  25. data/lib/command/maintenance_on.rb +8 -4
  26. data/lib/command/no_command.rb +1 -0
  27. data/lib/command/ps.rb +5 -1
  28. data/lib/command/run.rb +20 -23
  29. data/lib/command/run_detached.rb +38 -30
  30. data/lib/command/setup_app.rb +3 -3
  31. data/lib/command/version.rb +1 -0
  32. data/lib/core/config.rb +194 -66
  33. data/lib/core/controlplane.rb +28 -7
  34. data/lib/core/controlplane_api.rb +13 -1
  35. data/lib/core/controlplane_api_direct.rb +18 -2
  36. data/lib/core/helpers.rb +16 -0
  37. data/lib/core/shell.rb +25 -3
  38. data/lib/cpl/version.rb +1 -1
  39. data/lib/cpl.rb +32 -3
  40. data/lib/generator_templates/Dockerfile +27 -0
  41. data/lib/generator_templates/controlplane.yml +57 -0
  42. data/lib/generator_templates/entrypoint.sh +8 -0
  43. data/lib/generator_templates/templates/gvc.yml +21 -0
  44. data/lib/generator_templates/templates/postgres.yml +176 -0
  45. data/lib/generator_templates/templates/rails.yml +36 -0
  46. data/script/check_cpln_links +45 -0
  47. metadata +14 -3
data/lib/command/run.rb CHANGED
@@ -10,6 +10,7 @@ module Command
10
10
  app_option(required: true),
11
11
  image_option,
12
12
  workload_option,
13
+ location_option,
13
14
  use_local_token_option,
14
15
  terminal_size_option
15
16
  ].freeze
@@ -17,7 +18,6 @@ module Command
17
18
  LONG_DESCRIPTION = <<~DESC
18
19
  - Runs one-off **_interactive_** replicas (analog of `heroku run`)
19
20
  - Uses `Standard` workload type and `cpln exec` as the execution method, with CLI streaming
20
- - May not work correctly with tasks that last over 5 minutes (there's a Control Plane scaling bug at the moment)
21
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
22
 
23
23
  > **IMPORTANT:** Useful for development where it's needed for interaction, and where network connection drops and
@@ -29,56 +29,53 @@ module Command
29
29
  cpl run -a $APP_NAME
30
30
 
31
31
  # Need to quote COMMAND if setting ENV value or passing args.
32
- cpl run 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
33
-
34
- # COMMAND may also be passed at the end.
35
32
  cpl run -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
36
33
 
37
34
  # Runs command, displays output, and exits shell.
38
- cpl run ls / -a $APP_NAME
39
- cpl run rails db:migrate:status -a $APP_NAME
35
+ cpl run -a $APP_NAME -- ls /
36
+ cpl run -a $APP_NAME -- rails db:migrate:status
40
37
 
41
38
  # Runs command and keeps shell open.
42
- cpl run rails c -a $APP_NAME
39
+ cpl run -a $APP_NAME -- rails c
43
40
 
44
41
  # Uses a different image (which may not be promoted yet).
45
- cpl run rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
46
- cpl run rails db:migrate -a $APP_NAME --image latest # Latest sequential image
42
+ cpl run -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
43
+ cpl run -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image
47
44
 
48
45
  # Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
49
- cpl run bash -a $APP_NAME -w other-workload
46
+ cpl run -a $APP_NAME -w other-workload -- bash
50
47
 
51
48
  # Overrides remote CPLN_TOKEN env variable with local token.
52
49
  # Useful when superuser rights are needed in remote container.
53
- cpl run bash -a $APP_NAME --use-local-token
50
+ cpl run -a $APP_NAME --use-local-token -- bash
54
51
  ```
55
52
  EX
56
53
 
57
- attr_reader :location, :workload, :one_off, :container
54
+ attr_reader :location, :workload_to_clone, :workload_clone, :container
58
55
 
59
56
  def call # rubocop:disable Metrics/MethodLength
60
- @location = config[:default_location]
61
- @workload = config.options["workload"] || config[:one_off_workload]
62
- @one_off = "#{workload}-run-#{rand(1000..9999)}"
57
+ @location = config.location
58
+ @workload_to_clone = config.options["workload"] || config[:one_off_workload]
59
+ @workload_clone = "#{workload_to_clone}-run-#{random_four_digits}"
63
60
 
64
- step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
61
+ step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
65
62
  clone_workload
66
63
  end
67
64
 
68
- wait_for_workload(one_off)
69
- wait_for_replica(one_off, location)
65
+ wait_for_workload(workload_clone)
66
+ wait_for_replica(workload_clone, location)
70
67
  run_in_replica
71
68
  ensure
72
69
  progress.puts
73
- ensure_workload_deleted(one_off)
70
+ ensure_workload_deleted(workload_clone)
74
71
  end
75
72
 
76
73
  private
77
74
 
78
75
  def clone_workload # rubocop:disable Metrics/MethodLength
79
76
  # Create a base copy of workload props
80
- spec = cp.fetch_workload!(workload).fetch("spec")
81
- container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
77
+ spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
78
+ container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
82
79
  @container = container_spec["name"]
83
80
 
84
81
  # remove other containers if any
@@ -111,7 +108,7 @@ module Command
111
108
  end
112
109
 
113
110
  # Create workload clone
114
- cp.apply_hash("kind" => "workload", "name" => one_off, "spec" => spec)
111
+ cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
115
112
  end
116
113
 
117
114
  def runner_script # rubocop:disable Metrics/MethodLength
@@ -141,7 +138,7 @@ module Command
141
138
  def run_in_replica
142
139
  progress.puts("Connecting...\n\n")
143
140
  command = %(bash -c 'eval "$CONTROLPLANE_RUNNER"')
144
- cp.workload_exec(one_off, location: location, container: container, command: command)
141
+ cp.workload_exec(workload_clone, location: location, container: container, command: command)
145
142
  end
146
143
  end
147
144
  end
@@ -9,7 +9,9 @@ module Command
9
9
  app_option(required: true),
10
10
  image_option,
11
11
  workload_option,
12
- use_local_token_option
12
+ location_option,
13
+ use_local_token_option,
14
+ clean_on_failure_option
13
15
  ].freeze
14
16
  DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
15
17
  LONG_DESCRIPTION = <<~DESC
@@ -18,50 +20,46 @@ module Command
18
20
  - Implemented with only async execution methods, more suitable for production tasks
19
21
  - Has alternative log fetch implementation with only JSON-polling and no WebSockets
20
22
  - Less responsive but more stable, useful for CI tasks
23
+ - Deletes the workload whenever finished with success
24
+ - Deletes the workload whenever finished with failure by default
25
+ - Use `--no-clean-on-failure` to disable cleanup to help with debugging failed runs
21
26
  DESC
22
27
  EXAMPLES = <<~EX
23
28
  ```sh
24
29
  cpl run:detached rails db:prepare -a $APP_NAME
25
30
 
26
31
  # Need to quote COMMAND if setting ENV value or passing args.
27
- cpl run:detached 'LOG_LEVEL=warn rails db:migrate' -a $APP_NAME
28
-
29
- # COMMAND may also be passed at the end.
30
32
  cpl run:detached -a $APP_NAME -- 'LOG_LEVEL=warn rails db:migrate'
31
33
 
32
34
  # Uses a different image (which may not be promoted yet).
33
- cpl run:detached rails db:migrate -a $APP_NAME --image appimage:123 # Exact image name
34
- cpl run:detached rails db:migrate -a $APP_NAME --image latest # Latest sequential image
35
+ cpl run:detached -a $APP_NAME --image appimage:123 -- rails db:migrate # Exact image name
36
+ cpl run:detached -a $APP_NAME --image latest -- rails db:migrate # Latest sequential image
35
37
 
36
38
  # Uses a different workload than `one_off_workload` from `.controlplane/controlplane.yml`.
37
- cpl run:detached rails db:migrate:status -a $APP_NAME -w other-workload
39
+ cpl run:detached -a $APP_NAME -w other-workload -- rails db:migrate:status
38
40
 
39
41
  # Overrides remote CPLN_TOKEN env variable with local token.
40
42
  # Useful when superuser rights are needed in remote container.
41
- cpl run:detached rails db:migrate:status -a $APP_NAME --use-local-token
43
+ cpl run:detached -a $APP_NAME --use-local-token -- rails db:migrate:status
42
44
  ```
43
45
  EX
44
46
 
45
47
  WORKLOAD_SLEEP_CHECK = 2
46
48
 
47
- attr_reader :location, :workload, :one_off, :container
49
+ attr_reader :location, :workload_to_clone, :workload_clone, :container
48
50
 
49
- def call # rubocop:disable Metrics/MethodLength
50
- @location = config[:default_location]
51
- @workload = config.options["workload"] || config[:one_off_workload]
52
- @one_off = "#{workload}-runner-#{rand(1000..9999)}"
51
+ def call
52
+ @location = config.location
53
+ @workload_to_clone = config.options["workload"] || config[:one_off_workload]
54
+ @workload_clone = "#{workload_to_clone}-runner-#{random_four_digits}"
53
55
 
54
- step("Cloning workload '#{workload}' on app '#{config.options[:app]}' to '#{one_off}'") do
56
+ step("Cloning workload '#{workload_to_clone}' on app '#{config.options[:app]}' to '#{workload_clone}'") do
55
57
  clone_workload
56
58
  end
57
59
 
58
- wait_for_workload(one_off)
60
+ wait_for_workload(workload_clone)
59
61
  show_logs_waiting
60
62
  ensure
61
- if cp.fetch_workload(one_off)
62
- progress.puts
63
- ensure_workload_deleted(one_off)
64
- end
65
63
  exit(1) if @crashed
66
64
  end
67
65
 
@@ -69,8 +67,8 @@ module Command
69
67
 
70
68
  def clone_workload # rubocop:disable Metrics/MethodLength
71
69
  # Get base specs of workload
72
- spec = cp.fetch_workload!(workload).fetch("spec")
73
- container_spec = spec["containers"].detect { _1["name"] == workload } || spec["containers"].first
70
+ spec = cp.fetch_workload!(workload_to_clone).fetch("spec")
71
+ container_spec = spec["containers"].detect { _1["name"] == workload_to_clone } || spec["containers"].first
74
72
  @container = container_spec["name"]
75
73
 
76
74
  # remove other containers if any
@@ -104,7 +102,7 @@ module Command
104
102
  container_spec["env"] << { "name" => "CONTROLPLANE_RUNNER", "value" => runner_script }
105
103
 
106
104
  # Create workload clone
107
- cp.apply_hash("kind" => "workload", "name" => one_off, "spec" => spec)
105
+ cp.apply_hash("kind" => "workload", "name" => workload_clone, "spec" => spec)
108
106
  end
109
107
 
110
108
  def runner_script # rubocop:disable Metrics/MethodLength
@@ -118,12 +116,20 @@ module Command
118
116
  end
119
117
 
120
118
  script += <<~SHELL
121
- if ! eval "#{args_join(config.args)}"; then echo "----- CRASHED -----"; fi
122
-
123
- echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
124
- sleep 10 # grace time for logs propagation
125
- curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
126
- while true; do sleep 1; done # wait for SIGTERM
119
+ crashed=0
120
+ if ! eval "#{args_join(config.args)}"; then
121
+ crashed=1
122
+ echo "----- CRASHED -----"
123
+ fi
124
+ clean_on_failure=#{config.options[:clean_on_failure] ? 1 : 0}
125
+ if [ $crashed -eq 0 ] || [ $clean_on_failure -eq 1 ]; then
126
+ echo "-- FINISHED RUNNER SCRIPT, DELETING WORKLOAD --"
127
+ sleep 30 # grace time for logs propagation
128
+ curl ${CPLN_ENDPOINT}${CPLN_WORKLOAD} -H "Authorization: ${CONTROLPLANE_TOKEN}" -X DELETE -s -o /dev/null
129
+ while true; do sleep 1; done # wait for SIGTERM
130
+ else
131
+ echo "-- FINISHED RUNNER SCRIPT --"
132
+ fi
127
133
  SHELL
128
134
 
129
135
  script
@@ -132,7 +138,8 @@ module Command
132
138
  def show_logs_waiting # rubocop:disable Metrics/MethodLength
133
139
  progress.puts("Scheduled, fetching logs (it's a cron job, so it may take up to a minute to start)...\n\n")
134
140
  begin
135
- while cp.fetch_workload(one_off)
141
+ @finished = false
142
+ while cp.fetch_workload(workload_clone) && !@finished
136
143
  sleep(WORKLOAD_SLEEP_CHECK)
137
144
  print_uniq_logs
138
145
  end
@@ -150,6 +157,7 @@ module Command
150
157
 
151
158
  (entries - @printed_log_entries).sort.each do |(_ts, val)|
152
159
  @crashed = true if val.match?(/^----- CRASHED -----$/)
160
+ @finished = true if val.match?(/^-- FINISHED RUNNER SCRIPT(, DELETING WORKLOAD)? --$/)
153
161
  puts val
154
162
  end
155
163
 
@@ -157,7 +165,7 @@ module Command
157
165
  end
158
166
 
159
167
  def normalized_log_entries(from:, to:)
160
- log = cp.log_get(workload: one_off, from: from, to: to)
168
+ log = cp.log_get(workload: workload_clone, from: from, to: to)
161
169
 
162
170
  log["data"]["result"]
163
171
  .each_with_object([]) { |obj, result| result.concat(obj["values"]) }
@@ -9,12 +9,12 @@ module Command
9
9
  DESCRIPTION = "Creates an app and all its workloads"
10
10
  LONG_DESCRIPTION = <<~DESC
11
11
  - Creates an app and all its workloads
12
- - Specify the templates for the app and workloads through `setup` in the `.controlplane/controlplane.yml` file
13
- - This should 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)
12
+ - Specify the templates for the app and workloads through `setup_app_templates` in the `.controlplane/controlplane.yml` file
13
+ - 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)
14
14
  DESC
15
15
 
16
16
  def call
17
- templates = config[:setup]
17
+ templates = config[:setup_app_templates]
18
18
 
19
19
  app = cp.fetch_gvc
20
20
  if app
@@ -8,6 +8,7 @@ module Command
8
8
  - Displays the current version of the CLI
9
9
  - Can also be done with `cpl --version` or `cpl -v`
10
10
  DESC
11
+ WITH_INFO_HEADER = false
11
12
 
12
13
  def call
13
14
  puts Cpl::VERSION
data/lib/core/config.rb CHANGED
@@ -1,24 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "helpers"
4
+
3
5
  class Config # rubocop:disable Metrics/ClassLength
4
- attr_reader :config, :current,
5
- :org, :org_comes_from_env, :app, :apps, :app_dir,
6
+ attr_reader :org_comes_from_env, :app_comes_from_env,
6
7
  # command line options
7
- :args, :options
8
+ :args, :options, :required_options
9
+
10
+ include Helpers
8
11
 
9
- CONFIG_FILE_LOCATIION = ".controlplane/controlplane.yml"
12
+ CONFIG_FILE_LOCATION = ".controlplane/controlplane.yml"
10
13
 
11
- def initialize(args, options)
14
+ def initialize(args, options, required_options)
12
15
  @args = args
13
16
  @options = options
14
- @org = options[:org]
15
- @org_comes_from_env = false
16
- @app = options[:app]
17
+ @required_options = required_options
17
18
 
18
- load_app_config
19
- load_apps
19
+ ensure_required_options!
20
20
 
21
21
  Shell.verbose_mode(options[:verbose])
22
+ trace_mode = options[:trace]
23
+ return unless trace_mode
24
+
25
+ ControlplaneApiDirect.trace = trace_mode
26
+ Shell.warn("Trace mode is enabled, this will print sensitive information to the console.")
27
+ end
28
+
29
+ def org
30
+ @org ||= load_org_from_options || load_org_from_env || load_org_from_file
31
+ end
32
+
33
+ def app
34
+ @app ||= load_app_from_options || load_app_from_env
35
+ end
36
+
37
+ def location
38
+ @location ||= load_location_from_options || load_location_from_env || load_location_from_file
39
+ end
40
+
41
+ def domain
42
+ @domain ||= load_domain_from_options || load_domain_from_file
22
43
  end
23
44
 
24
45
  def [](key)
@@ -41,91 +62,122 @@ class Config # rubocop:disable Metrics/ClassLength
41
62
  apps[app_name.to_sym]&.dig(:match_if_app_name_starts_with) || false
42
63
  end
43
64
 
44
- private
65
+ def app_dir
66
+ Pathname.new(config_file_path).parent.parent.to_s
67
+ end
45
68
 
46
- def ensure_current_config!
47
- raise "Can't find current config, please specify an app." unless current
69
+ def config
70
+ @config ||= begin
71
+ global_config = YAML.safe_load_file(config_file_path, symbolize_names: true, aliases: true)
72
+ ensure_config!(global_config)
73
+ ensure_config_apps!(global_config)
74
+
75
+ global_config
76
+ end
48
77
  end
49
78
 
50
- def ensure_current_config_app!(app_name)
51
- raise "Can't find app '#{app_name}' in 'controlplane.yml'." unless current
79
+ def apps
80
+ @apps ||= config[:apps].to_h do |app_name, app_options|
81
+ ensure_config_app!(app_name, app_options)
82
+
83
+ app_options_with_new_keys = app_options.to_h do |key, value|
84
+ new_key = new_option_keys[key]
85
+ new_key ? [new_key, value] : [key, value]
86
+ end
87
+
88
+ [app_name, app_options_with_new_keys]
89
+ end
52
90
  end
53
91
 
54
- def ensure_current_config_org!(app_name)
55
- return if @org
92
+ def current
93
+ return unless app
56
94
 
57
- raise "Can't find option 'cpln_org' for app '#{app_name}' in 'controlplane.yml', " \
58
- "and CPLN_ORG env var is not set."
95
+ @current ||= begin
96
+ app_config = find_app_config(app)
97
+ ensure_config_app!(app, app_config)
98
+
99
+ warn_deprecated_options(app_config)
100
+
101
+ app_config
102
+ end
59
103
  end
60
104
 
61
- def ensure_config!
62
- raise "'controlplane.yml' is empty." unless config
105
+ def app_matches?(app_name1, app_name2, app_options)
106
+ app_name1 && app_name2 &&
107
+ (app_name1.to_s == app_name2.to_s ||
108
+ (app_options[:match_if_app_name_starts_with] && app_name1.to_s.start_with?(app_name2.to_s))
109
+ )
63
110
  end
64
111
 
65
- def ensure_config_apps!
66
- raise "Can't find key 'apps' in 'controlplane.yml'." unless config[:apps]
112
+ def find_app_config(app_name1)
113
+ @app_configs ||= {}
114
+ @app_configs[app_name1] ||= apps.find do |app_name2, app_config|
115
+ app_matches?(app_name1, app_name2, app_config)
116
+ end&.last
67
117
  end
68
118
 
69
- def ensure_config_app!(app_name, app_options)
70
- raise "App '#{app_name}' is empty in 'controlplane.yml'." unless app_options
119
+ private
120
+
121
+ def ensure_current_config!
122
+ raise "Can't find current config, please specify an app." unless current
71
123
  end
72
124
 
73
- def app_matches_current?(app_name, app_options)
74
- app && (app_name.to_s == app || (app_options[:match_if_app_name_starts_with] && app.start_with?(app_name.to_s)))
125
+ def ensure_config!(global_config)
126
+ raise "'controlplane.yml' is empty." unless global_config
75
127
  end
76
128
 
77
- def pick_current_config(app_name, app_options)
78
- @current = app_options
79
- ensure_current_config_app!(app_name)
129
+ def ensure_config_apps!(global_config)
130
+ raise "Can't find key 'apps' in 'controlplane.yml'." unless global_config[:apps]
131
+ end
80
132
 
81
- if current.key?(:cpln_org)
82
- @org = current.fetch(:cpln_org)
83
- else
84
- @org = ENV.fetch("CPLN_ORG", nil)
85
- @org_comes_from_env = true
86
- end
87
- ensure_current_config_org!(app_name)
133
+ def ensure_config_app!(app_name, app_options)
134
+ raise "Can't find config for app '#{app_name}' in 'controlplane.yml'." unless app_options
88
135
  end
89
136
 
90
- def load_apps # rubocop:disable Metrics/MethodLength
91
- @apps = config[:apps].to_h do |app_name, app_options|
92
- ensure_config_app!(app_name, app_options)
137
+ def ensure_app!
138
+ return if app
93
139
 
94
- app_options_with_new_keys = app_options.to_h do |key, value|
95
- new_key = new_option_keys[key]
96
- new_key ? [new_key, value] : [key, value]
97
- end
98
-
99
- if app_matches_current?(app_name, app_options_with_new_keys)
100
- pick_current_config(app_name, app_options_with_new_keys)
101
- warn_deprecated_options(app_options)
102
- end
140
+ raise "No app provided. " \
141
+ "The app can be provided either through the CPLN_APP env var " \
142
+ "('allow_app_override_by_env' must be set to true in 'controlplane.yml'), " \
143
+ "or the --app command option."
144
+ end
103
145
 
104
- [app_name, app_options_with_new_keys]
105
- end
146
+ def ensure_org!
147
+ return if org
106
148
 
107
- ensure_current_config_app!(app) if app
149
+ raise "No org provided. " \
150
+ "The org can be provided either through the CPLN_ORG env var " \
151
+ "('allow_org_override_by_env' must be set to true in 'controlplane.yml'), " \
152
+ "the --org command option, " \
153
+ "or the 'cpln_org' key in 'controlplane.yml'."
108
154
  end
109
155
 
110
- def load_app_config
111
- config_file = find_app_config_file
112
- @config = YAML.safe_load_file(config_file, symbolize_names: true, aliases: true)
113
- @app_dir = Pathname.new(config_file).parent.parent.to_s
114
- ensure_config!
115
- ensure_config_apps!
156
+ def ensure_required_options! # rubocop:disable Metrics/CyclomaticComplexity
157
+ ensure_app! if required_options.include?(:app)
158
+ ensure_org! if required_options.include?(:org) || app
159
+
160
+ missing_str = required_options
161
+ .reject { |option_name| %i[org app].include?(option_name) || options.key?(option_name) }
162
+ .map { |option_name| "--#{option_name}" }
163
+ .join(", ")
164
+
165
+ raise "Required options missing: #{missing_str}" unless missing_str.empty?
116
166
  end
117
167
 
118
- def find_app_config_file
119
- path = Pathname.new(".").expand_path
168
+ def config_file_path # rubocop:disable Metrics/MethodLength
169
+ @config_file_path ||= begin
170
+ path = Pathname.new(".").expand_path
120
171
 
121
- loop do
122
- config_file = path + CONFIG_FILE_LOCATIION
123
- break config_file if File.file?(config_file)
172
+ loop do
173
+ config_file = path + CONFIG_FILE_LOCATION
174
+ break config_file if File.file?(config_file)
124
175
 
125
- path = path.parent
176
+ path = path.parent
126
177
 
127
- if path.root?
128
- raise "Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATIION}', please create it."
178
+ if path.root?
179
+ raise "Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATION}', please create it."
180
+ end
129
181
  end
130
182
  end
131
183
  end
@@ -135,10 +187,86 @@ class Config # rubocop:disable Metrics/ClassLength
135
187
  org: :cpln_org,
136
188
  location: :default_location,
137
189
  prefix: :match_if_app_name_starts_with,
190
+ setup: :setup_app_templates,
138
191
  old_image_retention_days: :image_retention_days
139
192
  }
140
193
  end
141
194
 
195
+ def load_app_from_env
196
+ app_from_env = strip_str_and_validate(ENV.fetch("CPLN_APP", nil))
197
+ return unless app_from_env
198
+
199
+ app_config = find_app_config(app_from_env)
200
+ ensure_config_app!(app_from_env, app_config)
201
+
202
+ key_exists = app_config.key?(:allow_app_override_by_env)
203
+ allowed_locally = key_exists && app_config[:allow_app_override_by_env]
204
+ allowed_globally = !key_exists && config[:allow_app_override_by_env]
205
+ return unless allowed_locally || allowed_globally
206
+
207
+ @app_comes_from_env = true
208
+
209
+ app_from_env
210
+ end
211
+
212
+ def load_app_from_options
213
+ app_from_options = strip_str_and_validate(options[:app])
214
+ return unless app_from_options
215
+
216
+ app_config = find_app_config(app_from_options)
217
+ ensure_config_app!(app_from_options, app_config)
218
+
219
+ app_from_options
220
+ end
221
+
222
+ def load_org_from_env
223
+ org_from_env = strip_str_and_validate(ENV.fetch("CPLN_ORG", nil))
224
+ return unless org_from_env
225
+
226
+ key_exists = current&.key?(:allow_org_override_by_env)
227
+ allowed_locally = key_exists && current[:allow_org_override_by_env]
228
+ allowed_globally = !key_exists && config[:allow_org_override_by_env]
229
+ return unless allowed_locally || allowed_globally
230
+
231
+ @org_comes_from_env = true
232
+
233
+ org_from_env
234
+ end
235
+
236
+ def load_org_from_options
237
+ strip_str_and_validate(options[:org])
238
+ end
239
+
240
+ def load_org_from_file
241
+ return unless current&.key?(:cpln_org)
242
+
243
+ strip_str_and_validate(current[:cpln_org])
244
+ end
245
+
246
+ def load_location_from_options
247
+ strip_str_and_validate(options[:location])
248
+ end
249
+
250
+ def load_location_from_env
251
+ strip_str_and_validate(ENV.fetch("CPLN_LOCATION", nil))
252
+ end
253
+
254
+ def load_location_from_file
255
+ return unless current&.key?(:default_location)
256
+
257
+ strip_str_and_validate(current.fetch(:default_location))
258
+ end
259
+
260
+ def load_domain_from_options
261
+ strip_str_and_validate(options[:domain])
262
+ end
263
+
264
+ def load_domain_from_file
265
+ return unless current&.key?(:default_domain)
266
+
267
+ strip_str_and_validate(current.fetch(:default_domain))
268
+ end
269
+
142
270
  def warn_deprecated_options(app_options)
143
271
  deprecated_option_keys = new_option_keys.select { |old_key| app_options.key?(old_key) }
144
272
  return if deprecated_option_keys.empty?