aptible-cli 0.24.1 → 0.24.3

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/test.yml +16 -1
  3. data/Gemfile.lock +1 -1
  4. data/lib/aptible/cli/agent.rb +9 -1
  5. data/lib/aptible/cli/helpers/app.rb +16 -1
  6. data/lib/aptible/cli/helpers/database.rb +20 -5
  7. data/lib/aptible/cli/helpers/environment.rb +28 -3
  8. data/lib/aptible/cli/helpers/operation.rb +11 -4
  9. data/lib/aptible/cli/helpers/telemetry.rb +58 -0
  10. data/lib/aptible/cli/helpers/token.rb +6 -0
  11. data/lib/aptible/cli/renderer.rb +5 -1
  12. data/lib/aptible/cli/resource_formatter.rb +7 -0
  13. data/lib/aptible/cli/subcommands/apps.rb +31 -2
  14. data/lib/aptible/cli/subcommands/backup.rb +9 -0
  15. data/lib/aptible/cli/subcommands/backup_retention_policy.rb +5 -0
  16. data/lib/aptible/cli/subcommands/config.rb +11 -0
  17. data/lib/aptible/cli/subcommands/db.rb +68 -3
  18. data/lib/aptible/cli/subcommands/deploy.rb +3 -0
  19. data/lib/aptible/cli/subcommands/endpoints.rb +26 -0
  20. data/lib/aptible/cli/subcommands/environment.rb +11 -0
  21. data/lib/aptible/cli/subcommands/log_drain.rb +26 -5
  22. data/lib/aptible/cli/subcommands/logs.rb +5 -0
  23. data/lib/aptible/cli/subcommands/maintenance.rb +5 -0
  24. data/lib/aptible/cli/subcommands/metric_drain.rb +25 -5
  25. data/lib/aptible/cli/subcommands/operation.rb +7 -0
  26. data/lib/aptible/cli/subcommands/rebuild.rb +3 -0
  27. data/lib/aptible/cli/subcommands/restart.rb +3 -0
  28. data/lib/aptible/cli/subcommands/services.rb +12 -3
  29. data/lib/aptible/cli/subcommands/ssh.rb +3 -0
  30. data/lib/aptible/cli/version.rb +1 -1
  31. data/spec/aptible/cli/subcommands/apps_spec.rb +13 -1
  32. data/spec/aptible/cli/subcommands/config_spec.rb +4 -2
  33. data/spec/aptible/cli/subcommands/db_spec.rb +7 -1
  34. data/spec/aptible/cli/subcommands/endpoints_spec.rb +9 -3
  35. data/spec/aptible/cli/subcommands/environment_spec.rb +3 -1
  36. data/spec/aptible/cli/subcommands/log_drain_spec.rb +15 -2
  37. data/spec/aptible/cli/subcommands/maintenance_spec.rb +3 -2
  38. data/spec/aptible/cli/subcommands/metric_drain_spec.rb +15 -2
  39. data/spec/aptible/cli/subcommands/operation_spec.rb +4 -2
  40. data/spec/aptible/cli/subcommands/services_spec.rb +3 -1
  41. data/spec/fabricators/account_fabricator.rb +8 -0
  42. data/spec/fabricators/app_fabricator.rb +9 -0
  43. data/spec/fabricators/database_fabricator.rb +8 -0
  44. data/spec/fabricators/log_drain_fabricator.rb +8 -0
  45. data/spec/fabricators/metric_drain_fabricator.rb +8 -0
  46. data/spec/spec_helper.rb +9 -1
  47. metadata +6 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 57fd2762ecb6e2608548c08837ffa4c9cc3b233b4c4c12aae197001acd6c090a
4
- data.tar.gz: a9612cf83e1fbd680b6b0cc41daab09a7028f581b3f52bb6663b176e77b9a321
3
+ metadata.gz: f23bd0dde480e09b51d6876d15ff0628739d632197ae600bc2bc567bd54d7ee9
4
+ data.tar.gz: 7c2952e17c06a6c9593a5cbcbb3898e333d2c5e2a6d32bff053cd42798e87287
5
5
  SHA512:
6
- metadata.gz: b044b0ead595595831ec2608c48cfe0782de00272f424b21c5e2c4c8f384d2d3778d5f1aa331ff3e55e77e3670582f49d13245ca10b77a6986efc4d360fb847e
7
- data.tar.gz: 0a8a57cfd54a701edd13355b3048d570d0ba6a8f4d64199c95ec24130c34e1baf7a08afc79142df48f6d3cc37099a75bb452a7ae22c32c3dac53a753b9ce17f6
6
+ metadata.gz: b41752dc657d784ed2cc6da0ed4bc2c54f730adc59b02de4120d30aed2c6626b93a25b6049551051a17f1f6c841c9cac200f70eb480c1330c37543440e3d431e
7
+ data.tar.gz: 0f3a5c06f634a87ff7768914f8d383a86bbee84a4c6dc511797f752bdc777f29bd4882d46e8b93e53124d35fba16e1a0589cae9355e16c17392c9026322e3b69
@@ -11,7 +11,7 @@ on:
11
11
  jobs:
12
12
  test:
13
13
  name: Build and Publish
14
- runs-on: ubuntu-20.04
14
+ runs-on: ubuntu-latest
15
15
  strategy:
16
16
  fail-fast: false
17
17
  matrix:
@@ -48,3 +48,18 @@ jobs:
48
48
  contains(needs.*.result, 'failure')
49
49
  || contains(needs.*.result, 'cancelled')
50
50
  }}
51
+
52
+ report-version:
53
+ runs-on: ubuntu-latest
54
+ if: github.ref == 'refs/heads/master'
55
+ steps:
56
+ - name: Report version
57
+ run: |
58
+ curl -sS -X POST -u "$ENFORCER_USERNAME:$ENFORCER_PASSWORD" \
59
+ -H "Content-Type: application/json" \
60
+ -d '{"name":"aptible-cli", "type":"client", "version": "${{ github.sha }}"}' \
61
+ "$ENFORCER_API/repo_version"
62
+ env:
63
+ ENFORCER_API: https://app-83237.aptible-test-leeroy.com
64
+ ENFORCER_USERNAME: aptible
65
+ ENFORCER_PASSWORD: ${{ secrets.ENFORCER_PASSWORD }}
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- aptible-cli (0.24.1)
4
+ aptible-cli (0.24.3)
5
5
  activesupport (>= 4.0, < 6.0)
6
6
  aptible-api (~> 1.6.5)
7
7
  aptible-auth (~> 1.2.5)
@@ -1,5 +1,6 @@
1
1
  require 'base64'
2
2
  require 'uri'
3
+ require 'logger'
3
4
 
4
5
  require 'aptible/auth'
5
6
  require 'thor'
@@ -8,6 +9,7 @@ require 'chronic_duration'
8
9
 
9
10
  require_relative 'helpers/ssh'
10
11
  require_relative 'helpers/token'
12
+ require_relative 'helpers/telemetry'
11
13
  require_relative 'helpers/operation'
12
14
  require_relative 'helpers/environment'
13
15
  require_relative 'helpers/app'
@@ -80,7 +82,13 @@ module Aptible
80
82
 
81
83
  def initialize(*)
82
84
  nag_toolbelt unless toolbelt?
83
- Aptible::Resource.configure { |conf| conf.user_agent = version_string }
85
+ Aptible::Resource.configure do |conf|
86
+ conf.user_agent = version_string
87
+ level = Logger::WARN
88
+ debug_level = ENV['APTIBLE_DEBUG']
89
+ level = debug_level if debug_level
90
+ conf.logger.tap { |l| l.level = level }
91
+ end
84
92
  warn_sso_enforcement
85
93
  super
86
94
  end
@@ -151,12 +151,27 @@ module Aptible
151
151
  service
152
152
  end
153
153
 
154
+ def apps_href
155
+ href = '/apps'
156
+ if Renderer.format != 'json'
157
+ href = '/apps?per_page=5000&no_embed=true'
158
+ end
159
+ href
160
+ end
161
+
162
+ def apps_all
163
+ Aptible::Api::App.all(
164
+ token: fetch_token,
165
+ href: apps_href
166
+ )
167
+ end
168
+
154
169
  def apps_from_handle(handle, environment)
155
170
  # TODO: This should probably use each_app for more efficiency.
156
171
  if environment
157
172
  environment.apps
158
173
  else
159
- Aptible::Api::App.all(token: fetch_token)
174
+ apps_all
160
175
  end.select { |a| a.handle == handle }
161
176
  end
162
177
 
@@ -31,12 +31,27 @@ module Aptible
31
31
  end
32
32
  end
33
33
 
34
- def databases_from_handle(handle, environment)
35
- if environment
36
- databases = environment.databases
37
- else
38
- databases = Aptible::Api::Database.all(token: fetch_token)
34
+ def databases_href
35
+ href = '/databases'
36
+ if Renderer.format != 'json'
37
+ href = '/databases?per_page=5000&no_embed=true'
39
38
  end
39
+ href
40
+ end
41
+
42
+ def databases_all
43
+ Aptible::Api::Database.all(
44
+ token: fetch_token,
45
+ href: databases_href
46
+ )
47
+ end
48
+
49
+ def databases_from_handle(handle, environment)
50
+ databases = if environment
51
+ environment.databases
52
+ else
53
+ databases_all
54
+ end
40
55
  databases.select { |a| a.handle == handle }
41
56
  end
42
57
 
@@ -6,6 +6,14 @@ module Aptible
6
6
  module Environment
7
7
  include Helpers::Token
8
8
 
9
+ def environment_href
10
+ href = '/accounts'
11
+ if Renderer.format != 'json'
12
+ href = '/accounts?per_page=5000&no_embed=true'
13
+ end
14
+ href
15
+ end
16
+
9
17
  def scoped_environments(options)
10
18
  if options[:environment]
11
19
  if (environment = environment_from_handle(options[:environment]))
@@ -14,7 +22,11 @@ module Aptible
14
22
  raise Thor::Error, 'Specified account does not exist'
15
23
  end
16
24
  else
17
- Aptible::Api::Account.all(token: fetch_token)
25
+ href = environment_href
26
+ Aptible::Api::Account.all(
27
+ token: fetch_token,
28
+ href: href
29
+ )
18
30
  end
19
31
  end
20
32
 
@@ -30,13 +42,26 @@ module Aptible
30
42
 
31
43
  def environment_from_handle(handle)
32
44
  return nil unless handle
33
- Aptible::Api::Account.all(token: fetch_token).find do |a|
45
+ href = environment_href
46
+ Aptible::Api::Account.all(token: fetch_token, href: href).find do |a|
34
47
  a.handle == handle
35
48
  end
36
49
  end
37
50
 
51
+ def environment_map(accounts)
52
+ acc_map = {}
53
+ accounts.each do |account|
54
+ acc_map[account.links.self.href] = account
55
+ end
56
+ acc_map
57
+ end
58
+
38
59
  def ensure_default_environment
39
- environments = Aptible::Api::Account.all(token: fetch_token)
60
+ href = environment_href
61
+ environments = Aptible::Api::Account.all(
62
+ token: fetch_token,
63
+ href: href
64
+ )
40
65
  case environments.count
41
66
  when 0
42
67
  e = 'No environments. Go to https://app.aptible.com/ to proceed'
@@ -38,8 +38,10 @@ module Aptible
38
38
  # operation failed, poll_for_success will immediately fall through to
39
39
  # the error message.
40
40
  unless code == 0
41
- e = 'Disconnected from logs, waiting for operation to complete'
42
- CLI.logger.warn e
41
+ msg = ['Disconnected from logs, waiting for operation to complete',
42
+ 'Once complete, the logs can be viewed here:',
43
+ " #{ui_log_url(operation)}"]
44
+ msg.each { |e| CLI.logger.warn e }
43
45
  poll_for_success(operation)
44
46
  end
45
47
  end
@@ -76,8 +78,9 @@ module Aptible
76
78
  res = http.request(Net::HTTP::Get.new(uri.request_uri, headers))
77
79
  # note: res body with a 200 is target redirect location for download
78
80
  if !res || res.code != '200' || res.body.nil?
79
- raise Thor::Error, 'Unable to retrieve the operation\'s logs. '\
80
- 'If the issue persists please contact support for assistance.'
81
+ raise Thor::Error, 'Unable to retrieve the operation\'s logs. ' \
82
+ 'If the issue persists please contact support for assistance, or ' \
83
+ "view them at #{ui_log_url(operation)}"
81
84
  end
82
85
  res
83
86
  end
@@ -96,6 +99,10 @@ module Aptible
96
99
  end
97
100
  res
98
101
  end
102
+
103
+ def ui_log_url(operation)
104
+ "https://app.aptible.com/operations/#{operation.id}"
105
+ end
99
106
  end
100
107
  end
101
108
  end
@@ -0,0 +1,58 @@
1
+ require 'httpclient'
2
+ require 'securerandom'
3
+ require 'uri'
4
+
5
+ module Aptible
6
+ module CLI
7
+ module Helpers
8
+ module Telemetry
9
+ def telemetry(cmd, options = {})
10
+ token_hash = decode_token
11
+ format = Renderer.format
12
+ format = 'text' if format.nil?
13
+ sub = token_hash[0]['sub']
14
+ parsed_url = URI.parse(sub)
15
+ path_components = parsed_url.path.split('/')
16
+ user_or_org_id = path_components.last
17
+ # https://github.com/aptible/aptible-resource/blob/7c3a79e6eee9c88aa7dbf332e550508f22a5b08d/lib/hyper_resource/modules/http.rb#L21
18
+ client = HTTPClient.new.tap do |c|
19
+ c.cookie_manager = nil
20
+ c.connect_timeout = 30
21
+ c.send_timeout = 45
22
+ c.keep_alive_timeout = 15
23
+ c.ssl_config.set_default_paths
24
+ end
25
+
26
+ value = {
27
+ 'email' => token_hash[0]['email'],
28
+ 'format' => format,
29
+ 'cmd' => cmd,
30
+ 'options' => options,
31
+ 'version' => version_string,
32
+ # https://stackoverflow.com/a/73973555
33
+ 'github' => ENV['GITHUB_ACTIONS'],
34
+ 'gitlab' => ENV['GITLAB_CI'],
35
+ 'travis' => ENV['TRAVIS'],
36
+ 'circleci' => ENV['CIRCLECI'],
37
+ 'ci' => ENV['CI']
38
+ }
39
+
40
+ begin
41
+ uri = URI('https://tuna.aptible.com/www/e')
42
+ client.get(
43
+ uri,
44
+ 'id' => SecureRandom.uuid,
45
+ 'user_id' => user_or_org_id,
46
+ 'type' => 'cli_telemetry',
47
+ 'url' => sub,
48
+ 'value' => value
49
+ )
50
+ rescue
51
+ # since this is just for telemetry we don't want to notify
52
+ # user of an error
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,4 +1,5 @@
1
1
  require 'aptible/auth'
2
+ require 'jwt'
2
3
 
3
4
  require_relative 'config_path'
4
5
 
@@ -46,6 +47,11 @@ module Aptible
46
47
  def token_file
47
48
  File.join(aptible_config_path, 'tokens.json').freeze
48
49
  end
50
+
51
+ def decode_token
52
+ tok = fetch_token
53
+ JWT.decode(tok, nil, false)
54
+ end
49
55
  end
50
56
  end
51
57
  end
@@ -9,8 +9,12 @@ module Aptible
9
9
  module Renderer
10
10
  FORMAT_VAR = 'APTIBLE_OUTPUT_FORMAT'.freeze
11
11
 
12
+ def self.format
13
+ ENV[FORMAT_VAR]
14
+ end
15
+
12
16
  def self.current
13
- case (format = ENV[FORMAT_VAR])
17
+ case format
14
18
  when 'json'
15
19
  Json.new
16
20
  when 'text'
@@ -108,6 +108,13 @@ module Aptible
108
108
  attach_account(node, account)
109
109
  end
110
110
 
111
+ def inject_database_minimal(node, database, account)
112
+ node.value('id', database.id)
113
+ node.value('handle', database.handle)
114
+ node.value('created_at', database.created_at)
115
+ attach_account(node, account)
116
+ end
117
+
111
118
  def inject_database(node, database, account)
112
119
  node.value('id', database.id)
113
120
  node.value('handle', database.handle)
@@ -7,17 +7,34 @@ module Aptible
7
7
  include Helpers::App
8
8
  include Helpers::Environment
9
9
  include Helpers::Token
10
+ include Helpers::Telemetry
10
11
 
11
12
  desc 'apps', 'List all applications'
12
13
  option :environment, aliases: '--env'
13
14
  def apps
15
+ telemetry(__method__, options)
16
+
14
17
  Formatter.render(Renderer.current) do |root|
15
18
  root.grouped_keyed_list(
16
19
  { 'environment' => 'handle' },
17
20
  'handle'
18
21
  ) do |node|
19
- scoped_environments(options).each do |account|
20
- account.each_app do |app|
22
+ accounts = scoped_environments(options)
23
+ acc_map = environment_map(accounts)
24
+
25
+ if Renderer.format == 'json'
26
+ accounts.each do |account|
27
+ account.each_app do |app|
28
+ node.object do |n|
29
+ ResourceFormatter.inject_app(n, app, account)
30
+ end
31
+ end
32
+ end
33
+ else
34
+ apps_all.each do |app|
35
+ account = acc_map[app.links.account.href]
36
+ next if account.nil?
37
+
21
38
  node.object do |n|
22
39
  ResourceFormatter.inject_app(n, app, account)
23
40
  end
@@ -30,6 +47,8 @@ module Aptible
30
47
  desc 'apps:create HANDLE', 'Create a new application'
31
48
  option :environment, aliases: '--env'
32
49
  define_method 'apps:create' do |handle|
50
+ telemetry(__method__, options.merge(handle: handle))
51
+
33
52
  environment = ensure_environment(options)
34
53
  app = environment.create_app(handle: handle)
35
54
 
@@ -56,6 +75,8 @@ module Aptible
56
75
  option :container_profile, type: :string,
57
76
  desc: 'Examples: m c r'
58
77
  define_method 'apps:scale' do |type|
78
+ telemetry(__method__, options.merge(type: type))
79
+
59
80
  service = ensure_service(options, type)
60
81
 
61
82
  container_count = options[:container_count]
@@ -89,6 +110,8 @@ module Aptible
89
110
  desc 'apps:deprovision', 'Deprovision an app'
90
111
  app_options
91
112
  define_method 'apps:deprovision' do
113
+ telemetry(__method__, options)
114
+
92
115
  app = ensure_app(options)
93
116
  CLI.logger.info "Deprovisioning #{app.handle}..."
94
117
  op = app.create_operation!(type: 'deprovision')
@@ -108,6 +131,12 @@ module Aptible
108
131
  ' drain destinations, you must restart the app.'
109
132
  option :environment, aliases: '--env'
110
133
  define_method 'apps:rename' do |old_handle, new_handle|
134
+ opts = options.merge(
135
+ old_handle: old_handle,
136
+ new_handle: new_handle
137
+ )
138
+ telemetry(__method__, opts)
139
+
111
140
  env = ensure_environment(options)
112
141
  app = ensure_app(options.merge(app: old_handle))
113
142
  app.update!(handle: new_handle)
@@ -6,6 +6,7 @@ module Aptible
6
6
  thor.class_eval do
7
7
  include Helpers::Token
8
8
  include Helpers::Database
9
+ include Helpers::Telemetry
9
10
 
10
11
  desc 'backup:restore BACKUP_ID ' \
11
12
  '[--environment ENVIRONMENT_HANDLE] [--handle HANDLE] ' \
@@ -24,6 +25,8 @@ module Aptible
24
25
  desc: 'Examples: m c r'
25
26
  option :iops, type: :numeric
26
27
  define_method 'backup:restore' do |backup_id|
28
+ telemetry(__method__, options.merge(backup_id: backup_id))
29
+
27
30
  backup = Aptible::Api::Backup.find(backup_id, token: fetch_token)
28
31
  raise Thor::Error, "Backup ##{backup_id} not found" if backup.nil?
29
32
 
@@ -74,6 +77,8 @@ module Aptible
74
77
  default: '99y',
75
78
  desc: 'Limit backups returned (example usage: 1w, 1y, etc.)'
76
79
  define_method 'backup:list' do |handle|
80
+ telemetry(__method__, options.merge(handle: handle))
81
+
77
82
  age = ChronicDuration.parse(options[:max_age])
78
83
  raise Thor::Error, "Invalid age: #{options[:max_age]}" if age.nil?
79
84
  min_created_at = Time.now - age
@@ -101,6 +106,8 @@ module Aptible
101
106
  desc: 'Limit backups returned '\
102
107
  '(example usage: 1w, 1y, etc.)'
103
108
  define_method 'backup:orphaned' do
109
+ telemetry(__method__, options)
110
+
104
111
  age = ChronicDuration.parse(options[:max_age])
105
112
  raise Thor::Error, "Invalid age: #{options[:max_age]}" if age.nil?
106
113
  min_created_at = Time.now - age
@@ -126,6 +133,8 @@ module Aptible
126
133
  desc 'backup:purge BACKUP_ID',
127
134
  'Permanently delete a backup and any copies of it'
128
135
  define_method 'backup:purge' do |backup_id|
136
+ telemetry(__method__, options.merge(backup_id: backup_id))
137
+
129
138
  backup = Aptible::Api::Backup.find(backup_id, token: fetch_token)
130
139
  raise Thor::Error, "Backup ##{backup_id} not found" if backup.nil?
131
140
 
@@ -10,10 +10,13 @@ module Aptible
10
10
  thor.class_eval do
11
11
  include Helpers::Environment
12
12
  include Term::ANSIColor
13
+ include Helpers::Telemetry
13
14
 
14
15
  desc 'backup_retention_policy [ENVIRONMENT_HANDLE]',
15
16
  'Show the current backup retention policy for the environment'
16
17
  define_method 'backup_retention_policy' do |env|
18
+ telemetry(__method__, options.merge(env: env))
19
+
17
20
  account = ensure_environment(environment: env)
18
21
  policy = account.backup_retention_policies.first
19
22
  unless policy
@@ -52,6 +55,8 @@ module Aptible
52
55
  desc: 'Do not prompt for confirmation if the new policy ' \
53
56
  'retains fewer backups than the current policy'
54
57
  define_method 'backup_retention_policy:set' do |env|
58
+ telemetry(__method__, options.merge(env: env))
59
+
55
60
  if options.empty?
56
61
  raise Thor::Error,
57
62
  'Please specify at least one attribute to change'
@@ -7,10 +7,13 @@ module Aptible
7
7
  thor.class_eval do
8
8
  include Helpers::Operation
9
9
  include Helpers::App
10
+ include Helpers::Telemetry
10
11
 
11
12
  desc 'config', "Print an app's current configuration"
12
13
  app_options
13
14
  def config
15
+ telemetry(__method__, options)
16
+
14
17
  app = ensure_app(options)
15
18
  config = app.current_configuration
16
19
  env = config ? config.env : {}
@@ -32,6 +35,8 @@ module Aptible
32
35
  "Print a specific key within an app's current configuration"
33
36
  app_options
34
37
  define_method 'config:get' do |*args|
38
+ telemetry(__method__, options)
39
+
35
40
  app = ensure_app(options)
36
41
  config = app.current_configuration
37
42
  env = config ? config.env : {}
@@ -49,6 +54,8 @@ module Aptible
49
54
  'Add an ENV variable to an app'
50
55
  app_options
51
56
  define_method 'config:add' do |*args|
57
+ telemetry(__method__, options)
58
+
52
59
  # FIXME: define_method - ?! Seriously, WTF Thor.
53
60
  app = ensure_app(options)
54
61
  env = extract_env(args)
@@ -61,6 +68,7 @@ module Aptible
61
68
  'Add an ENV variable to an app'
62
69
  app_options
63
70
  define_method 'config:set' do |*args|
71
+ telemetry(__method__, options)
64
72
  send('config:add', *args)
65
73
  end
66
74
 
@@ -68,6 +76,8 @@ module Aptible
68
76
  'Remove an ENV variable from an app'
69
77
  app_options
70
78
  define_method 'config:rm' do |*args|
79
+ telemetry(__method__, options)
80
+
71
81
  # FIXME: define_method - ?! Seriously, WTF Thor.
72
82
  app = ensure_app(options)
73
83
  env = Hash[args.map do |arg|
@@ -84,6 +94,7 @@ module Aptible
84
94
  'Remove an ENV variable from an app'
85
95
  app_options
86
96
  define_method 'config:unset' do |*args|
97
+ telemetry(__method__, options)
87
98
  send('config:rm', *args)
88
99
  end
89
100
  end