cpl 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/command/info.rb CHANGED
@@ -4,7 +4,6 @@ module Command
4
4
  class Info < Base # rubocop:disable Metrics/ClassLength
5
5
  NAME = "info"
6
6
  OPTIONS = [
7
- org_option,
8
7
  app_option
9
8
  ].freeze
10
9
  DESCRIPTION = "Displays the diff between defined/available apps/workloads (apps equal GVCs)"
@@ -17,7 +16,7 @@ module Command
17
16
  DESC
18
17
  EXAMPLES = <<~EX
19
18
  ```sh
20
- # Shows diff for all apps in all orgs.
19
+ # Shows diff for all apps in all orgs (based on `.controlplane/controlplane.yml`).
21
20
  cpl info
22
21
 
23
22
  # Shows diff for all apps in a specific org.
@@ -27,6 +26,7 @@ module Command
27
26
  cpl info -a $APP_NAME
28
27
  ```
29
28
  EX
29
+ WITH_INFO_HEADER = false
30
30
 
31
31
  def call
32
32
  @missing_apps_workloads = {}
@@ -81,15 +81,12 @@ module Command
81
81
  end
82
82
  end
83
83
 
84
- def orgs # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
84
+ def orgs # rubocop:disable Metrics/MethodLength
85
85
  result = []
86
86
 
87
- if config.options[:org]
88
- result.push(config.options[:org])
87
+ if config.org
88
+ result.push(config.org)
89
89
  else
90
- org_from_env = ENV.fetch("CPLN_ORG", nil)
91
- result.push(org_from_env) if org_from_env
92
-
93
90
  config.apps.each do |app_name, app_options|
94
91
  next if config.app && !app_matches?(config.app, app_name, app_options)
95
92
 
@@ -10,6 +10,7 @@ module Command
10
10
  LONG_DESCRIPTION = <<~DESC
11
11
  - Displays the latest image name
12
12
  DESC
13
+ WITH_INFO_HEADER = false
13
14
 
14
15
  def call
15
16
  puts latest_image
@@ -14,6 +14,7 @@ module Command
14
14
  - Optionally specify the maintenance workload through `maintenance_workload` in the `.controlplane/controlplane.yml` file (defaults to 'maintenance')
15
15
  - Maintenance mode is only supported for domains that use path based routing mode and have a route configured for the prefix '/' on either port 80 or 443
16
16
  DESC
17
+ WITH_INFO_HEADER = false
17
18
 
18
19
  def call # rubocop:disable Metrics/MethodLength
19
20
  one_off_workload = config[:one_off_workload]
@@ -9,6 +9,7 @@ module Command
9
9
  - Called when no command was specified
10
10
  DESC
11
11
  HIDE = true
12
+ WITH_INFO_HEADER = false
12
13
 
13
14
  def call
14
15
  if config.options[:version]
data/lib/command/ps.rb CHANGED
@@ -5,6 +5,7 @@ module Command
5
5
  NAME = "ps"
6
6
  OPTIONS = [
7
7
  app_option(required: true),
8
+ location_option,
8
9
  workload_option
9
10
  ].freeze
10
11
  DESCRIPTION = "Shows running replicas in app"
@@ -20,16 +21,19 @@ module Command
20
21
  cpl ps -a $APP_NAME -w $WORKLOAD_NAME
21
22
  ```
22
23
  EX
24
+ WITH_INFO_HEADER = false
23
25
 
24
26
  def call
25
27
  cp.fetch_gvc!
26
28
 
29
+ location = config.location
30
+
27
31
  workloads = [config.options[:workload]] if config.options[:workload]
28
32
  workloads ||= config[:app_workloads] + config[:additional_workloads]
29
33
  workloads.each do |workload|
30
34
  cp.fetch_workload!(workload)
31
35
 
32
- result = cp.workload_get_replicas(workload, location: config[:default_location])
36
+ result = cp.workload_get_replicas(workload, location: location)
33
37
  result["items"].each { |replica| puts replica }
34
38
  end
35
39
  end
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
@@ -57,7 +57,7 @@ module Command
57
57
  attr_reader :location, :workload, :one_off, :container
58
58
 
59
59
  def call # rubocop:disable Metrics/MethodLength
60
- @location = config[:default_location]
60
+ @location = config.location
61
61
  @workload = config.options["workload"] || config[:one_off_workload]
62
62
  @one_off = "#{workload}-run-#{rand(1000..9999)}"
63
63
 
@@ -9,6 +9,7 @@ module Command
9
9
  app_option(required: true),
10
10
  image_option,
11
11
  workload_option,
12
+ location_option,
12
13
  use_local_token_option
13
14
  ].freeze
14
15
  DESCRIPTION = "Runs one-off **_non-interactive_** replicas (close analog of `heroku run:detached`)"
@@ -47,7 +48,7 @@ module Command
47
48
  attr_reader :location, :workload, :one_off, :container
48
49
 
49
50
  def call # rubocop:disable Metrics/MethodLength
50
- @location = config[:default_location]
51
+ @location = config.location
51
52
  @workload = config.options["workload"] || config[:one_off_workload]
52
53
  @one_off = "#{workload}-runner-#{rand(1000..9999)}"
53
54
 
@@ -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,41 @@
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
22
39
  end
23
40
 
24
41
  def [](key)
@@ -41,91 +58,122 @@ class Config # rubocop:disable Metrics/ClassLength
41
58
  apps[app_name.to_sym]&.dig(:match_if_app_name_starts_with) || false
42
59
  end
43
60
 
44
- private
45
-
46
- def ensure_current_config!
47
- raise "Can't find current config, please specify an app." unless current
61
+ def app_dir
62
+ Pathname.new(config_file_path).parent.parent.to_s
48
63
  end
49
64
 
50
- def ensure_current_config_app!(app_name)
51
- raise "Can't find app '#{app_name}' in 'controlplane.yml'." unless current
65
+ def config
66
+ @config ||= begin
67
+ global_config = YAML.safe_load_file(config_file_path, symbolize_names: true, aliases: true)
68
+ ensure_config!(global_config)
69
+ ensure_config_apps!(global_config)
70
+
71
+ global_config
72
+ end
52
73
  end
53
74
 
54
- def ensure_current_config_org!(app_name)
55
- return if @org
75
+ def apps
76
+ @apps ||= config[:apps].to_h do |app_name, app_options|
77
+ ensure_config_app!(app_name, app_options)
56
78
 
57
- raise "Can't find option 'cpln_org' for app '#{app_name}' in 'controlplane.yml', " \
58
- "and CPLN_ORG env var is not set."
79
+ app_options_with_new_keys = app_options.to_h do |key, value|
80
+ new_key = new_option_keys[key]
81
+ new_key ? [new_key, value] : [key, value]
82
+ end
83
+
84
+ [app_name, app_options_with_new_keys]
85
+ end
59
86
  end
60
87
 
61
- def ensure_config!
62
- raise "'controlplane.yml' is empty." unless config
88
+ def current
89
+ return unless app
90
+
91
+ @current ||= begin
92
+ app_config = find_app_config(app)
93
+ ensure_config_app!(app, app_config)
94
+
95
+ warn_deprecated_options(app_config)
96
+
97
+ app_config
98
+ end
63
99
  end
64
100
 
65
- def ensure_config_apps!
66
- raise "Can't find key 'apps' in 'controlplane.yml'." unless config[:apps]
101
+ private
102
+
103
+ def ensure_current_config!
104
+ raise "Can't find current config, please specify an app." unless current
67
105
  end
68
106
 
69
- def ensure_config_app!(app_name, app_options)
70
- raise "App '#{app_name}' is empty in 'controlplane.yml'." unless app_options
107
+ def ensure_config!(global_config)
108
+ raise "'controlplane.yml' is empty." unless global_config
71
109
  end
72
110
 
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)))
111
+ def ensure_config_apps!(global_config)
112
+ raise "Can't find key 'apps' in 'controlplane.yml'." unless global_config[:apps]
75
113
  end
76
114
 
77
- def pick_current_config(app_name, app_options)
78
- @current = app_options
79
- ensure_current_config_app!(app_name)
115
+ def ensure_config_app!(app_name, app_options)
116
+ raise "Can't find config for app '#{app_name}' in 'controlplane.yml'." unless app_options
117
+ end
80
118
 
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)
119
+ def app_matches?(app_name1, app_name2, app_options)
120
+ app_name1 && app_name2 &&
121
+ (app_name1.to_s == app_name2.to_s ||
122
+ (app_options[:match_if_app_name_starts_with] && app_name1.to_s.start_with?(app_name2.to_s))
123
+ )
88
124
  end
89
125
 
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)
126
+ def find_app_config(app_name1)
127
+ @app_configs ||= {}
128
+ @app_configs[app_name1] ||= apps.find do |app_name2, app_config|
129
+ app_matches?(app_name1, app_name2, app_config)
130
+ end&.last
131
+ end
93
132
 
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
133
+ def ensure_app!
134
+ return if app
98
135
 
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
136
+ raise "No app provided. " \
137
+ "The app can be provided either through the CPLN_APP env var " \
138
+ "('allow_app_override_by_env' must be set to true in 'controlplane.yml'), " \
139
+ "or the --app command option."
140
+ end
103
141
 
104
- [app_name, app_options_with_new_keys]
105
- end
142
+ def ensure_org!
143
+ return if org
106
144
 
107
- ensure_current_config_app!(app) if app
145
+ raise "No org provided. " \
146
+ "The org can be provided either through the CPLN_ORG env var " \
147
+ "('allow_org_override_by_env' must be set to true in 'controlplane.yml'), " \
148
+ "the --org command option, " \
149
+ "or the 'cpln_org' key in 'controlplane.yml'."
108
150
  end
109
151
 
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!
152
+ def ensure_required_options! # rubocop:disable Metrics/CyclomaticComplexity
153
+ ensure_app! if required_options.include?(:app)
154
+ ensure_org! if required_options.include?(:org) || app
155
+
156
+ missing_str = required_options
157
+ .reject { |option_name| %i[org app].include?(option_name) || options.key?(option_name) }
158
+ .map { |option_name| "--#{option_name}" }
159
+ .join(", ")
160
+
161
+ raise "Required options missing: #{missing_str}" unless missing_str.empty?
116
162
  end
117
163
 
118
- def find_app_config_file
119
- path = Pathname.new(".").expand_path
164
+ def config_file_path # rubocop:disable Metrics/MethodLength
165
+ @config_file_path ||= begin
166
+ path = Pathname.new(".").expand_path
120
167
 
121
- loop do
122
- config_file = path + CONFIG_FILE_LOCATIION
123
- break config_file if File.file?(config_file)
168
+ loop do
169
+ config_file = path + CONFIG_FILE_LOCATION
170
+ break config_file if File.file?(config_file)
124
171
 
125
- path = path.parent
172
+ path = path.parent
126
173
 
127
- if path.root?
128
- raise "Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATIION}', please create it."
174
+ if path.root?
175
+ raise "Can't find project config file at 'project_folder/#{CONFIG_FILE_LOCATION}', please create it."
176
+ end
129
177
  end
130
178
  end
131
179
  end
@@ -135,10 +183,76 @@ class Config # rubocop:disable Metrics/ClassLength
135
183
  org: :cpln_org,
136
184
  location: :default_location,
137
185
  prefix: :match_if_app_name_starts_with,
186
+ setup: :setup_app_templates,
138
187
  old_image_retention_days: :image_retention_days
139
188
  }
140
189
  end
141
190
 
191
+ def load_app_from_env
192
+ app_from_env = strip_str_and_validate(ENV.fetch("CPLN_APP", nil))
193
+ return unless app_from_env
194
+
195
+ app_config = find_app_config(app_from_env)
196
+ ensure_config_app!(app_from_env, app_config)
197
+
198
+ key_exists = app_config.key?(:allow_app_override_by_env)
199
+ allowed_locally = key_exists && app_config[:allow_app_override_by_env]
200
+ allowed_globally = !key_exists && config[:allow_app_override_by_env]
201
+ return unless allowed_locally || allowed_globally
202
+
203
+ @app_comes_from_env = true
204
+
205
+ app_from_env
206
+ end
207
+
208
+ def load_app_from_options
209
+ app_from_options = strip_str_and_validate(options[:app])
210
+ return unless app_from_options
211
+
212
+ app_config = find_app_config(app_from_options)
213
+ ensure_config_app!(app_from_options, app_config)
214
+
215
+ app_from_options
216
+ end
217
+
218
+ def load_org_from_env
219
+ org_from_env = strip_str_and_validate(ENV.fetch("CPLN_ORG", nil))
220
+ return unless org_from_env
221
+
222
+ key_exists = current&.key?(:allow_org_override_by_env)
223
+ allowed_locally = key_exists && current[:allow_org_override_by_env]
224
+ allowed_globally = !key_exists && config[:allow_org_override_by_env]
225
+ return unless allowed_locally || allowed_globally
226
+
227
+ @org_comes_from_env = true
228
+
229
+ org_from_env
230
+ end
231
+
232
+ def load_org_from_options
233
+ strip_str_and_validate(options[:org])
234
+ end
235
+
236
+ def load_org_from_file
237
+ return unless current&.key?(:cpln_org)
238
+
239
+ strip_str_and_validate(current[:cpln_org])
240
+ end
241
+
242
+ def load_location_from_options
243
+ strip_str_and_validate(options[:location])
244
+ end
245
+
246
+ def load_location_from_env
247
+ strip_str_and_validate(ENV.fetch("CPLN_LOCATION", nil))
248
+ end
249
+
250
+ def load_location_from_file
251
+ return unless current&.key?(:default_location)
252
+
253
+ strip_str_and_validate(current.fetch(:default_location))
254
+ end
255
+
142
256
  def warn_deprecated_options(app_options)
143
257
  deprecated_option_keys = new_option_keys.select { |old_key| app_options.key?(old_key) }
144
258
  return if deprecated_option_keys.empty?
@@ -23,9 +23,10 @@ class Controlplane # rubocop:disable Metrics/ClassLength
23
23
  end
24
24
 
25
25
  def profile_create(profile, token)
26
+ sensitive_data_pattern = /(?<=--token )(\S+)/
26
27
  cmd = "cpln profile create #{profile} --token #{token}"
27
28
  cmd += " > /dev/null" if Shell.should_hide_output?
28
- perform!(cmd)
29
+ perform!(cmd, sensitive_data_pattern: sensitive_data_pattern)
29
30
  end
30
31
 
31
32
  def profile_delete(profile)
@@ -47,6 +48,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
47
48
  # https://docs.controlplane.com/guides/push-image#step-2
48
49
  # Might need to use `docker buildx build` if compatiblitity issues arise
49
50
  cmd = "docker build --platform=linux/amd64 -t #{image} -f #{dockerfile}"
51
+ cmd += " --progress=plain" if ControlplaneApiDirect.trace
50
52
 
51
53
  build_args.each { |build_arg| cmd += " --build-arg #{build_arg}" }
52
54
  cmd += " #{config.app_dir}"
@@ -234,6 +236,16 @@ class Controlplane # rubocop:disable Metrics/ClassLength
234
236
  perform!(cmd)
235
237
  end
236
238
 
239
+ # volumeset
240
+
241
+ def fetch_volumesets(a_gvc = gvc)
242
+ api.list_volumesets(org: org, gvc: a_gvc)
243
+ end
244
+
245
+ def delete_volumeset(volumeset, a_gvc = gvc)
246
+ api.delete_volumeset(org: org, gvc: a_gvc, volumeset: volumeset)
247
+ end
248
+
237
249
  # domain
238
250
 
239
251
  def find_domain_route(data)
@@ -346,8 +358,8 @@ class Controlplane # rubocop:disable Metrics/ClassLength
346
358
  system(cmd)
347
359
  end
348
360
 
349
- def perform!(cmd)
350
- Shell.debug("CMD", cmd)
361
+ def perform!(cmd, sensitive_data_pattern: nil)
362
+ Shell.debug("CMD", cmd, sensitive_data_pattern: sensitive_data_pattern)
351
363
 
352
364
  system(cmd) || exit(false)
353
365
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class ControlplaneApi
3
+ class ControlplaneApi # rubocop:disable Metrics/ClassLength
4
4
  def gvc_list(org:)
5
5
  api_json("/org/#{org}/gvc", method: :get)
6
6
  end
@@ -86,6 +86,14 @@ class ControlplaneApi
86
86
  api_json("/org/#{org}/gvc/#{gvc}/workload/#{workload}", method: :delete)
87
87
  end
88
88
 
89
+ def list_volumesets(org:, gvc:)
90
+ api_json("/org/#{org}/gvc/#{gvc}/volumeset", method: :get)
91
+ end
92
+
93
+ def delete_volumeset(org:, gvc:, volumeset:)
94
+ api_json("/org/#{org}/gvc/#{gvc}/volumeset/#{volumeset}", method: :delete)
95
+ end
96
+
89
97
  def list_domains(org:)
90
98
  api_json("/org/#{org}/domain", method: :get)
91
99
  end
@@ -17,7 +17,12 @@ class ControlplaneApiDirect
17
17
 
18
18
  API_TOKEN_REGEX = /^[\w\-._]+$/.freeze
19
19
 
20
- def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLength
20
+ class << self
21
+ attr_accessor :trace
22
+ end
23
+
24
+ def call(url, method:, host: :api, body: nil) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
25
+ trace = ControlplaneApiDirect.trace
21
26
  uri = URI("#{api_host(host)}#{url}")
22
27
  request = API_METHODS[method].new(uri)
23
28
  request["Content-Type"] = "application/json"
@@ -26,7 +31,11 @@ class ControlplaneApiDirect
26
31
 
27
32
  Shell.debug(method.upcase, "#{uri} #{body&.to_json}")
28
33
 
29
- response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
34
+ http = Net::HTTP.new(uri.hostname, uri.port)
35
+ http.use_ssl = uri.scheme == "https"
36
+ http.set_debug_output($stdout) if trace
37
+
38
+ response = http.start { |ht| ht.request(request) }
30
39
 
31
40
  case response
32
41
  when Net::HTTPOK
@@ -35,6 +44,9 @@ class ControlplaneApiDirect
35
44
  true
36
45
  when Net::HTTPNotFound
37
46
  nil
47
+ when Net::HTTPForbidden
48
+ org = self.class.parse_org(url)
49
+ raise("Double check your org #{org}. #{response} #{response.body}")
38
50
  else
39
51
  raise("#{response} #{response.body}")
40
52
  end
@@ -65,4 +77,8 @@ class ControlplaneApiDirect
65
77
  remove_class_variable(:@@api_token) if defined?(@@api_token)
66
78
  end
67
79
  # rubocop:enable Style/ClassVars
80
+
81
+ def self.parse_org(url)
82
+ url.match(%r{^/org/([^/]+)})[1]
83
+ end
68
84
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Helpers
4
+ def strip_str_and_validate(str)
5
+ return str if str.nil?
6
+
7
+ str = str.strip
8
+ str.empty? ? nil : str
9
+ end
10
+ end
data/lib/core/shell.rb CHANGED
@@ -36,7 +36,7 @@ class Shell
36
36
  end
37
37
 
38
38
  def self.confirm(message)
39
- shell.yes?("#{message} (y/n)")
39
+ shell.yes?("#{message} (y/N)")
40
40
  end
41
41
 
42
42
  def self.warn(message)
@@ -55,11 +55,33 @@ class Shell
55
55
  @verbose = verbose
56
56
  end
57
57
 
58
- def self.debug(prefix, message)
59
- stderr.puts("\n[#{color(prefix, :red)}] #{message}") if verbose
58
+ def self.debug(prefix, message, sensitive_data_pattern: nil)
59
+ return unless verbose
60
+
61
+ filtered_message = hide_sensitive_data(message, sensitive_data_pattern)
62
+ stderr.puts("\n[#{color(prefix, :red)}] #{filtered_message}")
60
63
  end
61
64
 
62
65
  def self.should_hide_output?
63
66
  tmp_stderr && !verbose
64
67
  end
68
+
69
+ #
70
+ # Hide sensitive data based on the passed pattern
71
+ #
72
+ # @param [String] message
73
+ # The message to get processed.
74
+ # @param [Regexp, nil] pattern
75
+ # The regular expression to be used. If not provided, no filter gets applied.
76
+ #
77
+ # @return [String]
78
+ # Filtered message.
79
+ #
80
+ # @example
81
+ # hide_sensitive_data("--token abcd", /(?<=--token )(\S+)/)
82
+ def self.hide_sensitive_data(message, pattern = nil)
83
+ return message unless pattern.is_a?(Regexp)
84
+
85
+ message.gsub(pattern, "XXXXXXX")
86
+ end
65
87
  end
data/lib/cpl/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpl
4
- VERSION = "1.1.2"
4
+ VERSION = "1.2.0"
5
5
  MIN_CPLN_VERSION = "0.0.71"
6
6
  end