diff --git a/README.md b/README.md index 1a7a69ed..93662ed8 100644 --- a/README.md +++ b/README.md @@ -159,10 +159,19 @@ Here's a complete example of all supported config keys explained for the `contro ```yaml # Keys beginning with "cpln_" correspond to your settings in Control Plane. +# Global settings that apply to `cpl` usage. +# You can opt out of allowing the use of CPLN_ORG and CPLN_APP env vars +# to avoid any accidents with the wrong org / app. +allow_org_override_by_env: true +allow_app_override_by_env: true + aliases: common: &common - # Organization name for staging (customize to your needs). - # Production apps will use a different organization, specified below, for security. + # Organization for staging and QA apps is typically set as an alias. + # Production apps will use a different organization, specified in `apps`, for security. + # Change this value to your organization name + # or set the CPLN_ORG env var and it will override this for all `cpl` commands + # (provided that `allow_org_override_by_env` is set to `true`). cpln_org: my-org-staging # Example apps use only one location. Control Plane offers the ability to use multiple locations. diff --git a/examples/controlplane.yml b/examples/controlplane.yml index b76bbabf..95a334cc 100644 --- a/examples/controlplane.yml +++ b/examples/controlplane.yml @@ -1,5 +1,11 @@ # Keys beginning with "cpln_" correspond to your settings in Control Plane. +# Global settings that apply to `cpl` usage. +# You can opt out of allowing the use of CPLN_ORG and CPLN_APP env vars +# to avoid any accidents with the wrong org / app. +allow_org_override_by_env: true +allow_app_override_by_env: true + aliases: common: &common # Organization name for staging (customize to your needs). diff --git a/lib/command/base.rb b/lib/command/base.rb index e4cedf8a..205524f8 100644 --- a/lib/command/base.rb +++ b/lib/command/base.rb @@ -44,7 +44,7 @@ def self.common_options [org_option, verbose_option] end - def self.org_option(required: false) # rubocop:disable Metrics/MethodLength + def self.org_option(required: false) { name: :org, params: { @@ -52,13 +52,12 @@ def self.org_option(required: false) # rubocop:disable Metrics/MethodLength banner: "ORG_NAME", desc: "Organization name", type: :string, - required: required, - default: ENV.fetch("CPLN_ORG", nil) + required: required } } end - def self.app_option(required: false) # rubocop:disable Metrics/MethodLength + def self.app_option(required: false) { name: :app, params: { @@ -66,8 +65,7 @@ def self.app_option(required: false) # rubocop:disable Metrics/MethodLength banner: "APP_NAME", desc: "Application name", type: :string, - required: required, - default: ENV.fetch("CPLN_APP", nil) + required: required } } end diff --git a/lib/core/config.rb b/lib/core/config.rb index 7b47cce3..340b347e 100644 --- a/lib/core/config.rb +++ b/lib/core/config.rb @@ -2,22 +2,27 @@ class Config # rubocop:disable Metrics/ClassLength attr_reader :config, :current, - :org, :org_comes_from_env, :app, :apps, :app_dir, + :org, :org_comes_from_env, :app, :apps, :app_dir, :required_options, # command line options :args, :options CONFIG_FILE_LOCATIION = ".controlplane/controlplane.yml" - def initialize(args, options) + def initialize(args, options, required_options) @args = args @options = options - @org = options[:org]&.strip - @org_comes_from_env = true if ENV.fetch("CPLN_ORG", nil) - @app = options[:app]&.strip + @required_options = required_options load_app_config + + configure_org + configure_app + load_apps + ensure_org! if required_options.include?(:org) + ensure_required_options! + Shell.verbose_mode(options[:verbose]) end @@ -54,9 +59,9 @@ def ensure_current_config_app!(app_name) def ensure_current_config_org!(app_name) return if @org - raise "Can't find option 'cpln_org' for app '#{app_name}' in 'controlplane.yml', " \ - "and CPLN_ORG env var is not set. " \ - "The org can also be provided through --org." + raise "Can't find option 'cpln_org' for app '#{app_name}' in 'controlplane.yml'. " \ + "The org can also be provided either through --org " \ + "or the CPLN_ORG env var ('allow_org_override_by_env' must be set to true in 'controlplane.yml')." end def ensure_config! @@ -71,6 +76,57 @@ def ensure_config_app!(app_name, app_options) raise "App '#{app_name}' is empty in 'controlplane.yml'." unless app_options end + def ensure_org! + return if @org + + raise "No org provided. The org can be provided either through --org " \ + "or the CPLN_ORG env var ('allow_org_override_by_env' must be set to true in 'controlplane.yml')." + end + + def ensure_app! + return if @app + + raise "No app provided. The app can be provided either through --app " \ + "or the CPLN_APP env var ('allow_app_override_by_env' must be set to true in 'controlplane.yml')." + end + + def ensure_required_options! + missing_str = required_options + .reject { |option_name| %i[org app].include?(option_name) || options.key?(option_name) } + .map { |option_name| "--#{option_name}" } + .join(", ") + + raise "Required options missing: #{missing_str}" unless missing_str.empty? + end + + def non_empty_str_or_nil(str) + return str if str.nil? + + str.empty? ? nil : str + end + + def configure_org + if config[:allow_org_override_by_env] + org_from_env = non_empty_str_or_nil(ENV.fetch("CPLN_ORG", nil)&.strip) + if org_from_env + @org = org_from_env + @org_comes_from_env = true + end + end + + return if @org + + @org = non_empty_str_or_nil(options[:org]&.strip) + end + + def configure_app + @app = non_empty_str_or_nil(ENV.fetch("CPLN_APP", nil)&.strip) if config[:allow_app_override_by_env] + return if @app + + @app = non_empty_str_or_nil(options[:app]&.strip) + ensure_app! if required_options.include?(:app) + end + def app_matches_current?(app_name, app_options) app && (app_name.to_s == app || (app_options[:match_if_app_name_starts_with] && app.start_with?(app_name.to_s))) end @@ -81,7 +137,7 @@ def pick_current_config(app_name, app_options) return if @org - @org = current.fetch(:cpln_org)&.strip if current.key?(:cpln_org) + @org = non_empty_str_or_nil(current.fetch(:cpln_org)&.strip) if current.key?(:cpln_org) ensure_current_config_org!(app_name) end diff --git a/lib/cpl.rb b/lib/cpl.rb index 98813aaf..bd22cd71 100644 --- a/lib/cpl.rb +++ b/lib/cpl.rb @@ -157,8 +157,13 @@ def self.all_base_commands desc(usage, description, hide: hide) long_desc(long_description) + required_options = [] command_options.each do |option| - method_option(option[:name], **option[:params]) + # We'll handle required options manually in `Config` + required_options.push(option[:name]) if option[:params][:required] + + params = option[:params].reject { |key, _| key == :required } + method_option(option[:name], **params) end define_method(name_for_method) do |*provided_args| # rubocop:disable Metrics/MethodLength @@ -177,7 +182,7 @@ def self.all_base_commands raise_args_error.call(args, nil) if (args.empty? && requires_args) || (!args.empty? && !requires_args) begin - config = Config.new(args, options) + config = Config.new(args, options, required_options) show_info_header(config) if with_info_header