cpl 1.1.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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?